yq 除法运算符(/)完全指南:字符串分割与数值除法详解
【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq
导读
/(Divide)是 yq 中一个"一词两用"的二元运算符:当左侧操作数是字符串时,它执行字符串分割;当左侧操作数是整数或浮点数时,它执行算术除法。本文以 pkg/yqlib/doc/operators/divide.md 为主线,结合 operator_divide.go 的源码实现与 operator_divide_test.go 的测试用例,完整讲解/的三种典型场景(字符串分割、数值除法、除以零)、类型分派规则、边界错误场景与底层实现原理,让你在 YAML / JSON / XML / TOML 等任意格式的数据处理中准确驾驭这一运算符。
核心规则:行为取决于 LHS(左侧操作数)类型
Divide 运算符的行为由左侧操作数(LHS)的类型决定:
- 字符串(
!!str):按右侧操作数(分隔符)进行字符串分割,结果为数组(序列); - 数字(
!!int/!!float):进行算术除法,结果始终按浮点数计算; - 其他类型或类型不匹配:抛出错误。
这一点在源码中有清晰对应:divide函数首先检查 LHS 是否为!!null(null 不可参与除法),随后要求 LHS 与 RHS 均为标量(ScalarNode),再进入divideScalars依据具体标签分派(operator_divide.go)。
场景一:字符串分割(String split)
基本用法
给定sample.yml:
a: cat_meow b: _执行:
yq '.c = .a / .b' sample.yml输出:
a: cat_meow b: _ c: - cat - meow即:cat_meow按分隔符_被拆分为["cat", "meow"],并写入字段c。整个表达式可以理解为"把.a / .b的分割结果赋值给.c"。
底层实现:strings.Split
字符串分割的实现在 operator_strings.go 的split函数中,直接调用 Go 标准库strings.Split(value, spltStr):
func split(value string, spltStr string) (Kind, string, []*CandidateNode) { var contents []*CandidateNode if value != "" { var newStrings = strings.Split(value, spltStr) contents = make([]*CandidateNode, len(newStrings)) for index, str := range newStrings { contents[index] = &CandidateNode{Kind: ScalarNode, Tag: "!!str", Value: str} } } return SequenceNode, "!!seq", contents }值得注意的实现细节:
- 分割结果始终是一个
!!seq(序列/数组),每个元素都是!!str(字符串)标量,即使分割出来的片段看起来像数字,也不会被隐式转换类型; - 若被分割的字符串为空,则返回空序列(无任何子节点);
- 由于底层是
strings.Split,其语义与 Go 标准库一致:空分隔符会把字符串拆成单个字符序列,分隔符不出现时返回包含整个原字符串的单元素序列; - 分隔符本身可以是任意表达式求值出的字符串,因此
.a / .b中.b可以来自 YAML 字段、环境变量或函数调用结果。
快速实践
# 直接输出分割结果(不写入字段) yq '.a / "-"' sample.yml # 按字符拆解字符串 yq '.name / ""' sample.yml场景二:数值除法(Number division)
基本用法
数值除法过程中,结果始终按浮点数计算。
给定sample.yml:
a: 12 b: 2.5执行:
yq '.a = .a / .b' sample.yml输出:
a: 4.8 b: 2.5整数 12 除以浮点数 2.5,得到浮点结果 4.8,写入.a。
底层实现:统一走 float64
在divideScalars中(operator_divide.go),只要 LHS 与 RHS 的标签属于!!int或!!float组合,就会把两侧都通过strconv.ParseFloat(value, 64)解析为float64后相除:
lhsNum, err := strconv.ParseFloat(lhs.Value, 64) rhsNum, err := strconv.ParseFloat(rhs.Value, 64) quotient := lhsNum / rhsNum target.Tag = "!!float" target.Value = fmt.Sprintf("%v", quotient)由此可以总结出几条规则:
- 结果标签恒为
!!float:即使12 / 3得到整数 4,结果在 yq 中也会被标记为浮点类型(输出时可能显示为4,但类型语义是 float); - 除法结果不进行四舍五入或精度修饰,直接以
fmt.Sprintf("%v", quotient)输出 float64 的自然表示; - 整数、浮点可以任意混合:
int/int、int/float、float/int、float/float四种组合全部支持。
场景三:除以零(Division by zero)
基本用法
除以零不会报错,而是得到+Inf或-Inf(正负无穷)。
给定sample.yml:
a: 1 b: -1执行:
yq '.a = .a / 0 | .b = .b / 0' sample.yml输出:
a: +Inf b: -Inf1 / 0得到+Inf,-1 / 0得到-Inf。这是 float64 除法的标准 IEEE 754 语义:分子为正数得正无穷,分子为负数得负无穷。
底层实现
该行为同样来自strconv.ParseFloat+ float64 除法:Go 中浮点数除以零不会触发 panic,而是产生+Inf/-Inf(当分子不为零时)。随后fmt.Sprintf("%v", quotient)会将其格式化为+Inf/-Inf字面量,yq 将其作为浮点标量输出。
使用时的注意点:
- 若你的下游工具不接受
+Inf/-Inf,需要自行在表达式中用select或条件判断规避除零场景; 0 / 0在 float64 语义下会产生NaN(Not a Number),在编写健壮表达式时同样需要考虑。
源码级原理:类型分派与错误处理
分派逻辑总览
divide的核心逻辑(operator_divide.go)可以概括为:
- LHS 标签为
!!null→ 报错:"`!!null (path) cannot be divided by ..."; - LHS 与 RHS 必须都是标量节点,否则报错(map、seq 等容器类型不可参与除法);
- 两个标量进入
divideScalars依据标签分派:!!str/!!str→ 字符串分割(split);!!int/!!float组合 → 浮点除法;- 其余组合 → 报错,例如
!!int cannot be divided by !!str。
自定义标签(Custom Tags)的处理
divideScalars对带自定义标签(如!horse、!goat)的节点做了特殊处理(operator_divide.go):
- 通过
guessTagFromCustomType()猜测自定义标签底层的真实类型; - 若底层是字符串,则按字符串分割;若底层是数字,则按数值除法;
- 数值除法时,若 LHS 带自定义标签,结果的标签会保留为 LHS 的自定义标签(如
!horse),否则统一为!!float。
对应测试(operator_divide_test.go)验证了!horse cat_meow / !goat _按字符串分割、!horse 1.2 / !goat 2.3按数值除法且结果保留!horse标签的行为。
运算符注册:优先级与词法
从 operation.go 可以看到/的注册定义:
var divideOpType = &operationType{Type: "DIVIDE", NumArgs: 2, Precedence: 42, Handler: divideOperator}- 二元运算符(
NumArgs: 2); - 优先级 42,与
MULTIPLY、MODULO、ADD、SUBTRACT等算术运算符同级(operation.go); - 词法层面由 lexer_participle.go 的 token 规则
{"Divide",/, opToken(divideOpType), 0}识别/符号并映射到该运算类型。
多节点组合语义
从divideOperator的实现看,它通过crossFunction将 LHS 与 RHS 两侧所有匹配节点进行组合运算(operator_divide.go):
func divideOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { return crossFunction(d, context.ReadOnlyClone(), expressionNode, divide, false) }这意味着当表达式左侧或右侧匹配到多个节点(如数组元素、递归遍历结果)时,/会对每一对 LHS/RHS 组合分别执行除法或分割——从源码结构看,这与 multiply 等算术运算符保持一致的分发模型。
边界场景与易错点
结合 operator_divide_test.go 的测试用例,以下场景会直接报错:
| 场景 | 示例输入 | 错误信息 |
|---|---|---|
| 整数除以字符串 | a: 123,b: '2',表达式.a / .b | !!int cannot be divided by !!str |
| 字符串除以整数 | a: 2,b: '123',表达式.b / .a | !!str cannot be divided by !!int |
| map 除以数字 | a: {"a":1},b: 2 | !!map (a) cannot be divided by !!int (b) |
| 数组除以字符串 | a: [1,2],b: '2' | !!seq (a) cannot be divided by !!str (b) |
| null 参与除法 | LHS 为!!null | !!null (path) cannot be divided by ... |
实践建议:
- 字符串分割只接受字符串分隔符,数字分隔符需要先用
tostring之类的转换,或使用split相关的其他操作; - 数值除法不接受字符串操作数,即使字符串内容看起来是数字(如
'2'),也不会自动转换,必须先转为数值类型; - 容器类型(map / seq)不能直接参与除法;
- 除法运算不会改变原文档的其他字段,
=赋值只更新指定目标(测试中{a: 12, b: 2.5}除法后b保持不变)。
测试验证与运行方式
该运算符的完整行为由 operator_divide_test.go 中的divideOperatorScenarios表驱动测试覆盖,运行入口为TestDivideOperatorScenarios(operator_divide_test.go),涵盖:字符串分割、数值除法、除以零、自定义标签、锚点保留(如a: &horse [1]场景下除法后锚点仍被保留)、各类类型错误。测试同时通过documentOperatorScenarios机制生成并校验文档中的示例,保证 divide.md 与实现行为一致。
在仓库根目录运行以下命令即可复现验证:
go test ./pkg/yqlib/ -run TestDivideOperatorScenarios -v延伸阅读
- multiply-merge.md:同为高优先级算术运算符的
*(乘/合并)行为; - modulo.md:优先级相同的
%取模运算符; - string-operators.md:字符串相关的其他操作(如
+拼接、split相关能力); - operators.md:完整运算符参考索引。
小结
yq 的/运算符是一个按左侧类型自动分派的"双面"操作符:对字符串执行分割(底层为strings.Split,产出!!seq),对数字执行 float64 算术除法(结果恒为!!float),除零时按 IEEE 754 语义返回+Inf/-Inf而非报错。理解它的类型分派规则与边界错误场景,能帮助你在复杂数据管道中安全、准确地使用字符串拆分与数值计算能力。
【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考