diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3ace7d3..9bd9c81 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,4 +27,3 @@ repos: rev: v1.55.2 hooks: - id: golangci-lint - args: [--fast] diff --git a/classad/evaluator.go b/classad/evaluator.go index 17c7267..083da77 100644 --- a/classad/evaluator.go +++ b/classad/evaluator.go @@ -5,6 +5,7 @@ import ( "math" "github.com/PelicanPlatform/classad/ast" + "github.com/PelicanPlatform/classad/parser" ) // ValueType represents the type of a ClassAd value. @@ -891,8 +892,108 @@ func (e *Evaluator) evaluateOr(left, right Value) Value { return NewBoolValue(leftBool || rightBool) } +// evaluateUnparse handles the unparse() function which returns the string representation +// of an attribute's expression without evaluating it. +// unparse(attribute_name) - Returns the unparsed expression string for the given attribute +func (e *Evaluator) evaluateUnparse(args []ast.Expr) Value { + if len(args) != 1 { + return NewErrorValue() + } + + // The argument should be an attribute reference + attrRef, ok := args[0].(*ast.AttributeReference) + if !ok { + return NewErrorValue() + } + + // Determine which ClassAd to look up the attribute in based on scope + var targetClassAd *ClassAd + switch attrRef.Scope { + case ast.MyScope: + targetClassAd = e.classad + case ast.TargetScope: + if e.classad != nil { + targetClassAd = e.classad.target + } + case ast.ParentScope: + if e.classad != nil { + targetClassAd = e.classad.parent + } + default: + targetClassAd = e.classad + } + + if targetClassAd == nil { + return NewUndefinedValue() + } + + // Look up the attribute's expression (not evaluated) + expr := targetClassAd.lookupInternal(attrRef.Name) + if expr == nil { + return NewUndefinedValue() + } + + // Return the string representation of the expression + return NewStringValue(expr.String()) +} + +// evaluateEval handles the eval() function which parses and evaluates a string expression +// in the context of the current ClassAd. +// eval(string_expr) - Parses the string as a ClassAd expression and evaluates it +func (e *Evaluator) evaluateEval(args []ast.Expr) Value { + if len(args) != 1 { + return NewErrorValue() + } + + // Evaluate the argument to get the string to parse + val := e.Evaluate(args[0]) + + if val.IsError() { + return NewErrorValue() + } + if val.IsUndefined() { + return NewUndefinedValue() + } + if !val.IsString() { + return NewErrorValue() + } + + exprStr, _ := val.StringValue() + + // The parser expects a full ClassAd, so we wrap the expression in a temporary attribute + // Parse it as "[__eval_tmp = ]" and extract the expression + wrappedStr := "[__eval_tmp = " + exprStr + "]" + + node, err := parser.Parse(wrappedStr) + if err != nil { + return NewErrorValue() + } + + // The result should be a ClassAd + classAd, ok := node.(*ast.ClassAd) + if !ok || len(classAd.Attributes) != 1 { + return NewErrorValue() + } + + // Extract the expression from the temporary attribute + expr := classAd.Attributes[0].Value + + // Evaluate the expression in the current context + return e.Evaluate(expr) +} + // Built-in function evaluation func (e *Evaluator) evaluateFunctionCall(fc *ast.FunctionCall) Value { + // Handle unparse() specially - it needs access to the raw AST and ClassAd context + if fc.Name == "unparse" { + return e.evaluateUnparse(fc.Args) + } + + // Handle eval() specially - it needs to parse and evaluate in the current context + if fc.Name == "eval" { + return e.evaluateEval(fc.Args) + } + // Evaluate all arguments args := make([]Value, len(fc.Args)) for i, arg := range fc.Args { @@ -956,6 +1057,8 @@ func (e *Evaluator) evaluateFunctionCall(fc *ast.FunctionCall) Value { return builtinMember(args) case "stringListMember": return builtinStringListMember(args) + case "stringListIMember": + return builtinStringListIMember(args) // Pattern matching functions case "regexp": @@ -965,6 +1068,100 @@ func (e *Evaluator) evaluateFunctionCall(fc *ast.FunctionCall) Value { case "ifThenElse": return builtinIfThenElse(args) + // Type conversion functions + case "string": + return builtinString(args) + case "bool": + return builtinBool(args) + + // Math functions + case "pow": + return builtinPow(args) + case "quantize": + return builtinQuantize(args) + + // List aggregation functions + case "sum": + return builtinSum(args) + case "avg": + return builtinAvg(args) + case "min": + return builtinMin(args) + case "max": + return builtinMax(args) + + // String manipulation functions + case "join": + return builtinJoin(args) + case "split": + return builtinSplit(args) + case "splitUserName": + return builtinSplitUserName(args) + case "splitSlotName": + return builtinSplitSlotName(args) + case "strcmp": + return builtinStrcmp(args) + case "stricmp": + return builtinStricmp(args) + + // Version comparison functions + case "versioncmp": + return builtinVersioncmp(args) + case "version_gt": + return builtinVersionGT(args) + case "version_ge": + return builtinVersionGE(args) + case "version_lt": + return builtinVersionLT(args) + case "version_le": + return builtinVersionLE(args) + case "version_eq": + return builtinVersionEQ(args) + case "version_in_range": + return builtinVersionInRange(args) + + // Time formatting functions + case "formatTime": + return builtinFormatTime(args) + case "interval": + return builtinInterval(args) + + // List comparison functions + case "identicalMember": + return builtinIdenticalMember(args) + case "anyCompare": + return builtinAnyCompare(args) + case "allCompare": + return builtinAllCompare(args) + + // StringList functions + case "stringListSize": + return builtinStringListSize(args) + case "stringListSum": + return builtinStringListSum(args) + case "stringListAvg": + return builtinStringListAvg(args) + case "stringListMin": + return builtinStringListMin(args) + case "stringListMax": + return builtinStringListMax(args) + case "stringListsIntersect": + return builtinStringListsIntersect(args) + case "stringListSubsetMatch": + return builtinStringListSubsetMatch(args) + case "stringListRegexpMember": + return builtinStringListRegexpMember(args) + + // Regex functions + case "regexpMember": + return builtinRegexpMember(args) + case "regexps": + return builtinRegexps(args) + case "replace": + return builtinReplace(args) + case "replaceAll": + return builtinReplaceAll(args) + default: // Unknown function return NewErrorValue() diff --git a/classad/functions.go b/classad/functions.go index a10f6e5..7dd5485 100644 --- a/classad/functions.go +++ b/classad/functions.go @@ -2,6 +2,7 @@ package classad import ( + "fmt" "math" "math/rand" "regexp" @@ -487,6 +488,18 @@ func builtinStringListMember(args []Value) Value { return NewBoolValue(false) } +// builtinStringListIMember is a convenience wrapper for stringListMember with case-insensitive matching. +// stringListIMember(string item, string list) +// Returns true if item is in list (case-insensitive), false otherwise +func builtinStringListIMember(args []Value) Value { + if len(args) != 2 { + return NewErrorValue() + } + + // Call stringListMember with "i" option for case-insensitive matching + return builtinStringListMember([]Value{args[0], args[1], NewStringValue("i")}) +} + // builtinRegexp checks if a string matches a regular expression // regexp(string pattern, string target [, string options]) // Options can contain: @@ -577,3 +590,2023 @@ func builtinIfThenElse(args []Value) Value { } return args[2] } + +// builtinString converts any value to string +func builtinString(args []Value) Value { + if len(args) != 1 { + return NewErrorValue() + } + + if args[0].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() { + return NewErrorValue() + } + + // Convert based on type + if args[0].IsString() { + return args[0] + } + if args[0].IsInteger() { + val, _ := args[0].IntValue() + return NewStringValue(fmt.Sprintf("%d", val)) + } + if args[0].IsReal() { + val, _ := args[0].RealValue() + return NewStringValue(fmt.Sprintf("%g", val)) + } + if args[0].IsBool() { + val, _ := args[0].BoolValue() + if val { + return NewStringValue("true") + } + return NewStringValue("false") + } + + return NewErrorValue() +} + +// builtinBool converts any value to boolean +func builtinBool(args []Value) Value { + if len(args) != 1 { + return NewErrorValue() + } + + if args[0].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() { + return NewErrorValue() + } + + // Already boolean + if args[0].IsBool() { + return args[0] + } + + // Integer: 0 is false, non-zero is true + if args[0].IsInteger() { + val, _ := args[0].IntValue() + return NewBoolValue(val != 0) + } + + // Real: 0.0 is false, non-zero is true + if args[0].IsReal() { + val, _ := args[0].RealValue() + return NewBoolValue(val != 0.0) + } + + // String: "true" is true, "false" is false, others are ERROR + if args[0].IsString() { + str, _ := args[0].StringValue() + if str == "true" { + return NewBoolValue(true) + } + if str == "false" { + return NewBoolValue(false) + } + return NewErrorValue() + } + + return NewErrorValue() +} + +// builtinPow calculates base^exponent +func builtinPow(args []Value) Value { + if len(args) != 2 { + return NewErrorValue() + } + + if args[0].IsError() || args[1].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() || args[1].IsUndefined() { + return NewUndefinedValue() + } + + // Get base value as real + var base float64 + if args[0].IsInteger() { + val, _ := args[0].IntValue() + base = float64(val) + } else if args[0].IsReal() { + base, _ = args[0].RealValue() + } else { + return NewErrorValue() + } + + // Get exponent value + var exp float64 + expIsInt := false + if args[1].IsInteger() { + val, _ := args[1].IntValue() + exp = float64(val) + expIsInt = true + } else if args[1].IsReal() { + exp, _ = args[1].RealValue() + } else { + return NewErrorValue() + } + + result := math.Pow(base, exp) + + // Return integer if both inputs were integer and exp >= 0 + if expIsInt && args[0].IsInteger() && exp >= 0 { + return NewIntValue(int64(result)) + } + + return NewRealValue(result) +} + +// builtinQuantize computes ceiling(a/b)*b for scalars, or finds first value in list >= a +func builtinQuantize(args []Value) Value { + if len(args) != 2 { + return NewErrorValue() + } + + if args[0].IsError() || args[1].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() || args[1].IsUndefined() { + return NewUndefinedValue() + } + + // If second arg is a list + if args[1].IsList() { + list, _ := args[1].ListValue() + + // Get first numeric value from args[0] + var a float64 + if args[0].IsInteger() { + val, _ := args[0].IntValue() + a = float64(val) + } else if args[0].IsReal() { + a, _ = args[0].RealValue() + } else { + return NewErrorValue() + } + + // Find first value in list >= a + var lastVal Value + for _, item := range list { + if item.IsError() { + return NewErrorValue() + } + if item.IsUndefined() { + continue + } + + var itemVal float64 + if item.IsInteger() { + val, _ := item.IntValue() + itemVal = float64(val) + } else if item.IsReal() { + itemVal, _ = item.RealValue() + } else { + return NewErrorValue() + } + + if itemVal >= a { + return item + } + lastVal = item + } + + // No value >= a, compute integral multiple of last value + if lastVal.valueType != UndefinedValue { + var lastFloat float64 + if lastVal.IsInteger() { + val, _ := lastVal.IntValue() + lastFloat = float64(val) + } else if lastVal.IsReal() { + lastFloat, _ = lastVal.RealValue() + } + + quotient := a / lastFloat + result := math.Ceil(quotient) * lastFloat + + if lastVal.IsInteger() { + return NewIntValue(int64(result)) + } + return NewRealValue(result) + } + + return NewUndefinedValue() + } + + // Scalar case: compute ceiling(a/b)*b + var a, b float64 + aIsInt := args[0].IsInteger() + bIsInt := args[1].IsInteger() + + if args[0].IsInteger() { + val, _ := args[0].IntValue() + a = float64(val) + } else if args[0].IsReal() { + a, _ = args[0].RealValue() + } else { + return NewErrorValue() + } + + if args[1].IsInteger() { + val, _ := args[1].IntValue() + b = float64(val) + } else if args[1].IsReal() { + b, _ = args[1].RealValue() + } else { + return NewErrorValue() + } + + if b == 0 { + return NewErrorValue() + } + + quotient := a / b + result := math.Ceil(quotient) * b + + if bIsInt && aIsInt { + return NewIntValue(int64(result)) + } + return NewRealValue(result) +} + +// builtinSum sums numeric values in a list +func builtinSum(args []Value) Value { + if len(args) > 1 { + return NewErrorValue() + } + + if len(args) == 0 { + return NewIntValue(0) + } + + if args[0].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsList() { + return NewErrorValue() + } + + list, _ := args[0].ListValue() + if len(list) == 0 { + return NewIntValue(0) + } + + var sum float64 + hasReal := false + allUndefined := true + + for _, item := range list { + if item.IsError() { + return NewErrorValue() + } + if item.IsUndefined() { + continue + } + + allUndefined = false + + if item.IsInteger() { + val, _ := item.IntValue() + sum += float64(val) + } else if item.IsReal() { + val, _ := item.RealValue() + sum += val + hasReal = true + } else { + return NewErrorValue() + } + } + + if allUndefined { + return NewUndefinedValue() + } + + if hasReal { + return NewRealValue(sum) + } + return NewIntValue(int64(sum)) +} + +// builtinAvg computes average of numeric values in a list +func builtinAvg(args []Value) Value { + if len(args) > 1 { + return NewErrorValue() + } + + if len(args) == 0 { + return NewRealValue(0.0) + } + + if args[0].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsList() { + return NewErrorValue() + } + + list, _ := args[0].ListValue() + if len(list) == 0 { + return NewRealValue(0.0) + } + + var sum float64 + count := 0 + allUndefined := true + + for _, item := range list { + if item.IsError() { + return NewErrorValue() + } + if item.IsUndefined() { + continue + } + + allUndefined = false + + if item.IsInteger() { + val, _ := item.IntValue() + sum += float64(val) + count++ + } else if item.IsReal() { + val, _ := item.RealValue() + sum += val + count++ + } else { + return NewErrorValue() + } + } + + if allUndefined { + return NewUndefinedValue() + } + + if count == 0 { + return NewRealValue(0.0) + } + + return NewRealValue(sum / float64(count)) +} + +// builtinMin finds minimum value in a list +func builtinMin(args []Value) Value { + if len(args) > 1 { + return NewErrorValue() + } + + if len(args) == 0 { + return NewUndefinedValue() + } + + if args[0].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsList() { + return NewErrorValue() + } + + list, _ := args[0].ListValue() + if len(list) == 0 { + return NewUndefinedValue() + } + + var minVal float64 + var minItem Value + hasValue := false + hasReal := false + + for _, item := range list { + if item.IsError() { + return NewErrorValue() + } + if item.IsUndefined() { + continue + } + + var val float64 + if item.IsInteger() { + v, _ := item.IntValue() + val = float64(v) + } else if item.IsReal() { + val, _ = item.RealValue() + hasReal = true + } else { + return NewErrorValue() + } + + if !hasValue || val < minVal { + minVal = val + minItem = item + hasValue = true + } + } + + if !hasValue { + return NewUndefinedValue() + } + + if hasReal { + return NewRealValue(minVal) + } + return minItem +} + +// builtinMax finds maximum value in a list +func builtinMax(args []Value) Value { + if len(args) > 1 { + return NewErrorValue() + } + + if len(args) == 0 { + return NewUndefinedValue() + } + + if args[0].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsList() { + return NewErrorValue() + } + + list, _ := args[0].ListValue() + if len(list) == 0 { + return NewUndefinedValue() + } + + var maxVal float64 + var maxItem Value + hasValue := false + hasReal := false + + for _, item := range list { + if item.IsError() { + return NewErrorValue() + } + if item.IsUndefined() { + continue + } + + var val float64 + if item.IsInteger() { + v, _ := item.IntValue() + val = float64(v) + } else if item.IsReal() { + val, _ = item.RealValue() + hasReal = true + } else { + return NewErrorValue() + } + + if !hasValue || val > maxVal { + maxVal = val + maxItem = item + hasValue = true + } + } + + if !hasValue { + return NewUndefinedValue() + } + + if hasReal { + return NewRealValue(maxVal) + } + return maxItem +} + +// builtinJoin joins strings with a separator +// join(sep, arg1, arg2, ...) or join(sep, list) or join(list) +func builtinJoin(args []Value) Value { + if len(args) == 0 { + return NewErrorValue() + } + + // join(list) - no separator + if len(args) == 1 { + if args[0].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() { + return NewErrorValue() + } + if !args[0].IsList() { + return NewErrorValue() + } + + list, _ := args[0].ListValue() + var result strings.Builder + for _, item := range list { + if item.IsUndefined() { + continue + } + if item.IsString() { + str, _ := item.StringValue() + result.WriteString(str) + } else if item.IsInteger() { + val, _ := item.IntValue() + result.WriteString(fmt.Sprintf("%d", val)) + } else if item.IsReal() { + val, _ := item.RealValue() + result.WriteString(fmt.Sprintf("%g", val)) + } else if item.IsBool() { + val, _ := item.BoolValue() + if val { + result.WriteString("true") + } else { + result.WriteString("false") + } + } + } + return NewStringValue(result.String()) + } + + // Get separator + if !args[0].IsString() { + return NewErrorValue() + } + sep, _ := args[0].StringValue() + + // Two-argument form: join(separator, list) + if len(args) == 2 && args[1].IsList() { + if args[1].IsError() { + return NewErrorValue() + } + + list, _ := args[1].ListValue() + var parts []string + for _, item := range list { + if item.IsUndefined() { + continue + } + if item.IsString() { + str, _ := item.StringValue() + parts = append(parts, str) + } else if item.IsInteger() { + val, _ := item.IntValue() + parts = append(parts, fmt.Sprintf("%d", val)) + } else if item.IsReal() { + val, _ := item.RealValue() + parts = append(parts, fmt.Sprintf("%g", val)) + } else if item.IsBool() { + val, _ := item.BoolValue() + if val { + parts = append(parts, "true") + } else { + parts = append(parts, "false") + } + } + } + return NewStringValue(strings.Join(parts, sep)) + } + + // join(sep, arg1, arg2, ...) + var parts []string + for i := 1; i < len(args); i++ { + if args[i].IsError() { + return NewErrorValue() + } + if args[i].IsUndefined() { + continue + } + + if args[i].IsString() { + str, _ := args[i].StringValue() + parts = append(parts, str) + } else if args[i].IsInteger() { + val, _ := args[i].IntValue() + parts = append(parts, fmt.Sprintf("%d", val)) + } else if args[i].IsReal() { + val, _ := args[i].RealValue() + parts = append(parts, fmt.Sprintf("%g", val)) + } else if args[i].IsBool() { + val, _ := args[i].BoolValue() + if val { + parts = append(parts, "true") + } else { + parts = append(parts, "false") + } + } + } + + return NewStringValue(strings.Join(parts, sep)) +} + +// builtinSplit splits a string into a list +func builtinSplit(args []Value) Value { + if len(args) < 1 || len(args) > 2 { + return NewErrorValue() + } + + if args[0].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsString() { + return NewErrorValue() + } + + str, _ := args[0].StringValue() + + // Default delimiter is whitespace + if len(args) == 1 { + fields := strings.Fields(str) + var result []Value + for _, field := range fields { + result = append(result, NewStringValue(field)) + } + return NewListValue(result) + } + + // Custom delimiter + if !args[1].IsString() { + return NewErrorValue() + } + delim, _ := args[1].StringValue() + + // Split on any character in delimiter string + fields := strings.FieldsFunc(str, func(r rune) bool { + return strings.ContainsRune(delim, r) + }) + + var result []Value + for _, field := range fields { + result = append(result, NewStringValue(field)) + } + return NewListValue(result) +} + +// builtinSplitUserName splits "user@domain" into {"user", "domain"} +func builtinSplitUserName(args []Value) Value { + if len(args) != 1 { + return NewErrorValue() + } + + if args[0].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsString() { + return NewErrorValue() + } + + name, _ := args[0].StringValue() + parts := strings.SplitN(name, "@", 2) + + if len(parts) == 2 { + return NewListValue([]Value{ + NewStringValue(parts[0]), + NewStringValue(parts[1]), + }) + } + + return NewListValue([]Value{ + NewStringValue(name), + NewStringValue(""), + }) +} + +// builtinSplitSlotName splits "slot1@machine" into {"slot1", "machine"} +// If no @, returns {"", "name"} +func builtinSplitSlotName(args []Value) Value { + if len(args) != 1 { + return NewErrorValue() + } + + if args[0].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsString() { + return NewErrorValue() + } + + name, _ := args[0].StringValue() + parts := strings.SplitN(name, "@", 2) + + if len(parts) == 2 { + return NewListValue([]Value{ + NewStringValue(parts[0]), + NewStringValue(parts[1]), + }) + } + + return NewListValue([]Value{ + NewStringValue(""), + NewStringValue(name), + }) +} + +// builtinStrcmp compares strings (case-sensitive) +func builtinStrcmp(args []Value) Value { + if len(args) != 2 { + return NewErrorValue() + } + + if args[0].IsError() || args[1].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() || args[1].IsUndefined() { + return NewErrorValue() + } + + // Convert to strings + var str1, str2 string + if args[0].IsString() { + str1, _ = args[0].StringValue() + } else if args[0].IsInteger() { + val, _ := args[0].IntValue() + str1 = fmt.Sprintf("%d", val) + } else if args[0].IsReal() { + val, _ := args[0].RealValue() + str1 = fmt.Sprintf("%g", val) + } else { + return NewErrorValue() + } + + if args[1].IsString() { + str2, _ = args[1].StringValue() + } else if args[1].IsInteger() { + val, _ := args[1].IntValue() + str2 = fmt.Sprintf("%d", val) + } else if args[1].IsReal() { + val, _ := args[1].RealValue() + str2 = fmt.Sprintf("%g", val) + } else { + return NewErrorValue() + } + + result := strings.Compare(str1, str2) + return NewIntValue(int64(result)) +} + +// builtinStricmp compares strings (case-insensitive) +func builtinStricmp(args []Value) Value { + if len(args) != 2 { + return NewErrorValue() + } + + if args[0].IsError() || args[1].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() || args[1].IsUndefined() { + return NewErrorValue() + } + + // Convert to strings + var str1, str2 string + if args[0].IsString() { + str1, _ = args[0].StringValue() + } else if args[0].IsInteger() { + val, _ := args[0].IntValue() + str1 = fmt.Sprintf("%d", val) + } else if args[0].IsReal() { + val, _ := args[0].RealValue() + str1 = fmt.Sprintf("%g", val) + } else { + return NewErrorValue() + } + + if args[1].IsString() { + str2, _ = args[1].StringValue() + } else if args[1].IsInteger() { + val, _ := args[1].IntValue() + str2 = fmt.Sprintf("%d", val) + } else if args[1].IsReal() { + val, _ := args[1].RealValue() + str2 = fmt.Sprintf("%g", val) + } else { + return NewErrorValue() + } + + result := strings.Compare(strings.ToLower(str1), strings.ToLower(str2)) + return NewIntValue(int64(result)) +} + +// versionCompare implements HTCondor version comparison logic +// Lexicographic except numeric sequences are compared numerically +func versionCompare(left, right string) int { + i, j := 0, 0 + + for i < len(left) && j < len(right) { + // Check if both are at the start of a numeric sequence + if left[i] >= '0' && left[i] <= '9' && right[j] >= '0' && right[j] <= '9' { + // Count leading zeros + zeros1, zeros2 := 0, 0 + for i < len(left) && left[i] == '0' { + zeros1++ + i++ + } + for j < len(right) && right[j] == '0' { + zeros2++ + j++ + } + + // Extract remaining digits + numEnd1, numEnd2 := i, j + for numEnd1 < len(left) && left[numEnd1] >= '0' && left[numEnd1] <= '9' { + numEnd1++ + } + for numEnd2 < len(right) && right[numEnd2] >= '0' && right[numEnd2] <= '9' { + numEnd2++ + } + + // Compare numeric values + if i < numEnd1 || j < numEnd2 { + // At least one has non-zero digits + num1Len := numEnd1 - i + num2Len := numEnd2 - j + + if num1Len != num2Len { + return num1Len - num2Len + } + + // Same length, compare digit by digit + for k := 0; k < num1Len; k++ { + if left[i+k] != right[j+k] { + return int(left[i+k]) - int(right[j+k]) + } + } + + i = numEnd1 + j = numEnd2 + } else { + // Both are all zeros - more zeros means smaller + if zeros1 != zeros2 { + return zeros2 - zeros1 + } + i = numEnd1 + j = numEnd2 + } + } else { + // Lexicographic comparison + if left[i] != right[j] { + return int(left[i]) - int(right[j]) + } + i++ + j++ + } + } + + // One string is a prefix of the other + return len(left) - len(right) +} + +// builtinVersioncmp compares version strings +func builtinVersioncmp(args []Value) Value { + if len(args) != 2 { + return NewErrorValue() + } + + if args[0].IsError() || args[1].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() || args[1].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsString() || !args[1].IsString() { + return NewErrorValue() + } + + left, _ := args[0].StringValue() + right, _ := args[1].StringValue() + + result := versionCompare(left, right) + return NewIntValue(int64(result)) +} + +// builtinVersionGT checks if left > right +func builtinVersionGT(args []Value) Value { + result := builtinVersioncmp(args) + if result.IsError() || result.IsUndefined() { + return result + } + val, _ := result.IntValue() + return NewBoolValue(val > 0) +} + +// builtinVersionGE checks if left >= right +func builtinVersionGE(args []Value) Value { + result := builtinVersioncmp(args) + if result.IsError() || result.IsUndefined() { + return result + } + val, _ := result.IntValue() + return NewBoolValue(val >= 0) +} + +// builtinVersionLT checks if left < right +func builtinVersionLT(args []Value) Value { + result := builtinVersioncmp(args) + if result.IsError() || result.IsUndefined() { + return result + } + val, _ := result.IntValue() + return NewBoolValue(val < 0) +} + +// builtinVersionLE checks if left <= right +func builtinVersionLE(args []Value) Value { + result := builtinVersioncmp(args) + if result.IsError() || result.IsUndefined() { + return result + } + val, _ := result.IntValue() + return NewBoolValue(val <= 0) +} + +// builtinVersionEQ checks if left == right +func builtinVersionEQ(args []Value) Value { + result := builtinVersioncmp(args) + if result.IsError() || result.IsUndefined() { + return result + } + val, _ := result.IntValue() + return NewBoolValue(val == 0) +} + +// builtinVersionInRange checks if min <= version <= max +func builtinVersionInRange(args []Value) Value { + if len(args) != 3 { + return NewErrorValue() + } + + // Check version >= min + minCheck := builtinVersionGE([]Value{args[0], args[1]}) + if minCheck.IsError() || minCheck.IsUndefined() { + return minCheck + } + minOk, _ := minCheck.BoolValue() + + if !minOk { + return NewBoolValue(false) + } + + // Check version <= max + maxCheck := builtinVersionLE([]Value{args[0], args[2]}) + if maxCheck.IsError() || maxCheck.IsUndefined() { + return maxCheck + } + maxOk, _ := maxCheck.BoolValue() + + return NewBoolValue(maxOk) +} + +// builtinFormatTime formats a Unix timestamp +func builtinFormatTime(args []Value) Value { + if len(args) > 2 { + return NewErrorValue() + } + + // Get time (default to current time) + var t time.Time + if len(args) >= 1 && !args[0].IsUndefined() { + if args[0].IsError() { + return NewErrorValue() + } + if !args[0].IsInteger() { + return NewErrorValue() + } + timestamp, _ := args[0].IntValue() + t = time.Unix(timestamp, 0).UTC() + } else { + t = time.Now().UTC() + } + + // Get format string (default to "%c") + format := "%c" + if len(args) >= 2 { + if args[1].IsError() { + return NewErrorValue() + } + if args[1].IsUndefined() { + return NewErrorValue() + } + if !args[1].IsString() { + return NewErrorValue() + } + format, _ = args[1].StringValue() + } + + // Convert strftime format to Go format + result := convertStrftimeToGo(t, format) + return NewStringValue(result) +} + +// convertStrftimeToGo converts strftime format codes to Go time format +func convertStrftimeToGo(t time.Time, format string) string { + var result strings.Builder + i := 0 + + for i < len(format) { + if format[i] == '%' && i+1 < len(format) { + switch format[i+1] { + case '%': + result.WriteByte('%') + case 'a': + result.WriteString(t.Format("Mon")) + case 'A': + result.WriteString(t.Format("Monday")) + case 'b': + result.WriteString(t.Format("Jan")) + case 'B': + result.WriteString(t.Format("January")) + case 'c': + result.WriteString(t.Format("Mon Jan 2 15:04:05 2006")) + case 'd': + result.WriteString(t.Format("02")) + case 'H': + result.WriteString(t.Format("15")) + case 'I': + result.WriteString(t.Format("03")) + case 'j': + result.WriteString(fmt.Sprintf("%03d", t.YearDay())) + case 'm': + result.WriteString(t.Format("01")) + case 'M': + result.WriteString(t.Format("04")) + case 'p': + result.WriteString(t.Format("PM")) + case 'S': + result.WriteString(t.Format("05")) + case 'U', 'W': + // Week number - simplified + _, week := t.ISOWeek() + result.WriteString(fmt.Sprintf("%02d", week)) + case 'w': + result.WriteString(fmt.Sprintf("%d", t.Weekday())) + case 'x': + result.WriteString(t.Format("01/02/06")) + case 'X': + result.WriteString(t.Format("15:04:05")) + case 'y': + result.WriteString(t.Format("06")) + case 'Y': + result.WriteString(t.Format("2006")) + case 'Z': + result.WriteString(t.Format("MST")) + default: + result.WriteByte('%') + result.WriteByte(format[i+1]) + } + i += 2 + } else { + result.WriteByte(format[i]) + i++ + } + } + + return result.String() +} + +// builtinInterval formats seconds as "days+hh:mm:ss" +func builtinInterval(args []Value) Value { + if len(args) != 1 { + return NewErrorValue() + } + + if args[0].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsInteger() { + return NewErrorValue() + } + + seconds, _ := args[0].IntValue() + + days := seconds / 86400 + seconds %= 86400 + hours := seconds / 3600 + seconds %= 3600 + minutes := seconds / 60 + seconds %= 60 + + if days > 0 { + return NewStringValue(fmt.Sprintf("%d+%02d:%02d:%02d", days, hours, minutes, seconds)) + } + if hours > 0 { + return NewStringValue(fmt.Sprintf("%d:%02d:%02d", hours, minutes, seconds)) + } + if minutes > 0 { + return NewStringValue(fmt.Sprintf("%d:%02d", minutes, seconds)) + } + return NewStringValue(fmt.Sprintf("0:%02d", seconds)) +} + +// builtinIdenticalMember checks if m is in list using =?= (strict identity) +func builtinIdenticalMember(args []Value) Value { + if len(args) != 2 { + return NewErrorValue() + } + + if args[0].IsError() || args[1].IsError() { + return NewErrorValue() + } + + // First arg must be scalar + if args[0].IsList() || args[0].IsClassAd() { + return NewErrorValue() + } + + // Second arg must be list + if !args[1].IsList() { + return NewErrorValue() + } + + list, _ := args[1].ListValue() + + for _, item := range list { + // Strict identity check - same type and value + if args[0].valueType != item.valueType { + continue + } + + if args[0].valueType == IntegerValue { + v1, _ := args[0].IntValue() + v2, _ := item.IntValue() + if v1 == v2 { + return NewBoolValue(true) + } + } else if args[0].valueType == RealValue { + v1, _ := args[0].RealValue() + v2, _ := item.RealValue() + if v1 == v2 { + return NewBoolValue(true) + } + } else if args[0].valueType == StringValue { + v1, _ := args[0].StringValue() + v2, _ := item.StringValue() + if v1 == v2 { + return NewBoolValue(true) + } + } else if args[0].valueType == BooleanValue { + v1, _ := args[0].BoolValue() + v2, _ := item.BoolValue() + if v1 == v2 { + return NewBoolValue(true) + } + } else if args[0].valueType == UndefinedValue { + return NewBoolValue(true) + } + } + + return NewBoolValue(false) +} + +// builtinAnyCompare checks if any element in list satisfies comparison with t +// anyCompare(op, list, target) where op is "<", "<=", "==", "!=", ">=", ">" +func builtinAnyCompare(args []Value) Value { + if len(args) != 3 { + return NewErrorValue() + } + + if args[0].IsError() || args[1].IsError() || args[2].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() || args[1].IsUndefined() || args[2].IsUndefined() { + return NewUndefinedValue() + } + + if !args[0].IsString() || !args[1].IsList() { + return NewErrorValue() + } + + op, _ := args[0].StringValue() + list, _ := args[1].ListValue() + target := args[2] + + for _, item := range list { + if item.IsUndefined() { + continue + } + + result := compareValues(op, item, target) + if result.IsBool() { + match, _ := result.BoolValue() + if match { + return NewBoolValue(true) + } + } + } + + return NewBoolValue(false) +} + +// builtinAllCompare checks if all elements in list satisfy comparison with t +func builtinAllCompare(args []Value) Value { + if len(args) != 3 { + return NewErrorValue() + } + + if args[0].IsError() || args[1].IsError() || args[2].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() || args[1].IsUndefined() || args[2].IsUndefined() { + return NewUndefinedValue() + } + + if !args[0].IsString() || !args[1].IsList() { + return NewErrorValue() + } + + op, _ := args[0].StringValue() + list, _ := args[1].ListValue() + target := args[2] + + if len(list) == 0 { + return NewBoolValue(true) // vacuously true + } + + for _, item := range list { + if item.IsUndefined() { + continue + } + + result := compareValues(op, item, target) + if result.IsBool() { + match, _ := result.BoolValue() + if !match { + return NewBoolValue(false) + } + } else { + return NewBoolValue(false) + } + } + + return NewBoolValue(true) +} + +// compareValues performs comparison based on operator string +func compareValues(op string, left, right Value) Value { + // Handle numeric comparison + if left.IsNumber() && right.IsNumber() { + leftNum, _ := left.NumberValue() + rightNum, _ := right.NumberValue() + + switch op { + case "<": + return NewBoolValue(leftNum < rightNum) + case "<=": + return NewBoolValue(leftNum <= rightNum) + case "==": + return NewBoolValue(leftNum == rightNum) + case "!=": + return NewBoolValue(leftNum != rightNum) + case ">=": + return NewBoolValue(leftNum >= rightNum) + case ">": + return NewBoolValue(leftNum > rightNum) + } + } + + // Handle string comparison + if left.IsString() && right.IsString() { + leftStr, _ := left.StringValue() + rightStr, _ := right.StringValue() + cmp := strings.Compare(leftStr, rightStr) + + switch op { + case "<": + return NewBoolValue(cmp < 0) + case "<=": + return NewBoolValue(cmp <= 0) + case "==": + return NewBoolValue(cmp == 0) + case "!=": + return NewBoolValue(cmp != 0) + case ">=": + return NewBoolValue(cmp >= 0) + case ">": + return NewBoolValue(cmp > 0) + } + } + + // Handle boolean comparison + if left.IsBool() && right.IsBool() { + leftBool, _ := left.BoolValue() + rightBool, _ := right.BoolValue() + + switch op { + case "==": + return NewBoolValue(leftBool == rightBool) + case "!=": + return NewBoolValue(leftBool != rightBool) + } + } + + return NewErrorValue() +} + +// parseStringList splits a string list by delimiter (default comma) +func parseStringList(listStr, delimiter string) []string { + if delimiter == "" { + delimiter = "," + } + + parts := strings.Split(listStr, delimiter) + var result []string + for _, part := range parts { + result = append(result, strings.TrimSpace(part)) + } + return result +} + +// builtinStringListSize returns the number of elements in a string list +func builtinStringListSize(args []Value) Value { + if len(args) < 1 || len(args) > 2 { + return NewErrorValue() + } + + if args[0].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsString() { + return NewErrorValue() + } + + listStr, _ := args[0].StringValue() + delimiter := "," + + if len(args) == 2 { + if args[1].IsError() { + return NewErrorValue() + } + if args[1].IsUndefined() { + return NewUndefinedValue() + } + if !args[1].IsString() { + return NewErrorValue() + } + delimiter, _ = args[1].StringValue() + } + + parts := parseStringList(listStr, delimiter) + // Don't count empty strings + count := 0 + for _, part := range parts { + if part != "" { + count++ + } + } + return NewIntValue(int64(count)) +} + +// builtinStringListSum sums numeric values in a string list +func builtinStringListSum(args []Value) Value { + if len(args) < 1 || len(args) > 2 { + return NewErrorValue() + } + + if args[0].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsString() { + return NewErrorValue() + } + + listStr, _ := args[0].StringValue() + delimiter := "," + + if len(args) == 2 { + if args[1].IsError() { + return NewErrorValue() + } + if args[1].IsUndefined() { + return NewUndefinedValue() + } + if !args[1].IsString() { + return NewErrorValue() + } + delimiter, _ = args[1].StringValue() + } + + parts := parseStringList(listStr, delimiter) + var sum float64 + hasReal := false + + for _, part := range parts { + if part == "" { + continue + } + + var val float64 + if strings.Contains(part, ".") { + _, err := fmt.Sscanf(part, "%f", &val) + if err != nil { + continue + } + hasReal = true + } else { + var intVal int64 + _, err := fmt.Sscanf(part, "%d", &intVal) + if err != nil { + continue + } + val = float64(intVal) + } + sum += val + } + + if hasReal { + return NewRealValue(sum) + } + return NewIntValue(int64(sum)) +} + +// builtinStringListAvg computes average of numeric values in a string list +func builtinStringListAvg(args []Value) Value { + if len(args) < 1 || len(args) > 2 { + return NewErrorValue() + } + + if args[0].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsString() { + return NewErrorValue() + } + + listStr, _ := args[0].StringValue() + delimiter := "," + + if len(args) == 2 { + if args[1].IsError() { + return NewErrorValue() + } + if args[1].IsUndefined() { + return NewUndefinedValue() + } + if !args[1].IsString() { + return NewErrorValue() + } + delimiter, _ = args[1].StringValue() + } + + parts := parseStringList(listStr, delimiter) + var sum float64 + count := 0 + + for _, part := range parts { + if part == "" { + continue + } + + var val float64 + _, err := fmt.Sscanf(part, "%f", &val) + if err != nil { + continue + } + sum += val + count++ + } + + if count == 0 { + return NewRealValue(0.0) + } + + return NewRealValue(sum / float64(count)) +} + +// builtinStringListMin finds minimum numeric value in a string list +func builtinStringListMin(args []Value) Value { + if len(args) < 1 || len(args) > 2 { + return NewErrorValue() + } + + if args[0].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsString() { + return NewErrorValue() + } + + listStr, _ := args[0].StringValue() + delimiter := "," + + if len(args) == 2 { + if args[1].IsError() { + return NewErrorValue() + } + if args[1].IsUndefined() { + return NewUndefinedValue() + } + if !args[1].IsString() { + return NewErrorValue() + } + delimiter, _ = args[1].StringValue() + } + + parts := parseStringList(listStr, delimiter) + var minVal float64 + hasValue := false + hasReal := false + + for _, part := range parts { + if part == "" { + continue + } + + var val float64 + if strings.Contains(part, ".") { + _, err := fmt.Sscanf(part, "%f", &val) + if err != nil { + continue + } + hasReal = true + } else { + var intVal int64 + _, err := fmt.Sscanf(part, "%d", &intVal) + if err != nil { + continue + } + val = float64(intVal) + } + + if !hasValue || val < minVal { + minVal = val + hasValue = true + } + } + + if !hasValue { + return NewUndefinedValue() + } + + if hasReal { + return NewRealValue(minVal) + } + return NewIntValue(int64(minVal)) +} + +// builtinStringListMax finds maximum numeric value in a string list +func builtinStringListMax(args []Value) Value { + if len(args) < 1 || len(args) > 2 { + return NewErrorValue() + } + + if args[0].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsString() { + return NewErrorValue() + } + + listStr, _ := args[0].StringValue() + delimiter := "," + + if len(args) == 2 { + if args[1].IsError() { + return NewErrorValue() + } + if args[1].IsUndefined() { + return NewUndefinedValue() + } + if !args[1].IsString() { + return NewErrorValue() + } + delimiter, _ = args[1].StringValue() + } + + parts := parseStringList(listStr, delimiter) + var maxVal float64 + hasValue := false + hasReal := false + + for _, part := range parts { + if part == "" { + continue + } + + var val float64 + if strings.Contains(part, ".") { + _, err := fmt.Sscanf(part, "%f", &val) + if err != nil { + continue + } + hasReal = true + } else { + var intVal int64 + _, err := fmt.Sscanf(part, "%d", &intVal) + if err != nil { + continue + } + val = float64(intVal) + } + + if !hasValue || val > maxVal { + maxVal = val + hasValue = true + } + } + + if !hasValue { + return NewUndefinedValue() + } + + if hasReal { + return NewRealValue(maxVal) + } + return NewIntValue(int64(maxVal)) +} + +// builtinStringListsIntersect checks if two string lists have common elements +func builtinStringListsIntersect(args []Value) Value { + if len(args) < 2 || len(args) > 3 { + return NewErrorValue() + } + + if args[0].IsError() || args[1].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() || args[1].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsString() || !args[1].IsString() { + return NewErrorValue() + } + + list1Str, _ := args[0].StringValue() + list2Str, _ := args[1].StringValue() + delimiter := "," + + if len(args) == 3 { + if args[2].IsError() { + return NewErrorValue() + } + if args[2].IsUndefined() { + return NewUndefinedValue() + } + if !args[2].IsString() { + return NewErrorValue() + } + delimiter, _ = args[2].StringValue() + } + + list1 := parseStringList(list1Str, delimiter) + list2 := parseStringList(list2Str, delimiter) + + // Create a set from list2 for fast lookup + set := make(map[string]bool) + for _, item := range list2 { + if item != "" { + set[item] = true + } + } + + // Check if any item from list1 is in the set + for _, item := range list1 { + if item != "" && set[item] { + return NewBoolValue(true) + } + } + + return NewBoolValue(false) +} + +// builtinStringListSubsetMatch checks if list1 is a subset of list2 +func builtinStringListSubsetMatch(args []Value) Value { + if len(args) < 2 || len(args) > 3 { + return NewErrorValue() + } + + if args[0].IsError() || args[1].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() || args[1].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsString() || !args[1].IsString() { + return NewErrorValue() + } + + list1Str, _ := args[0].StringValue() + list2Str, _ := args[1].StringValue() + delimiter := "," + + if len(args) == 3 { + if args[2].IsError() { + return NewErrorValue() + } + if args[2].IsUndefined() { + return NewUndefinedValue() + } + if !args[2].IsString() { + return NewErrorValue() + } + delimiter, _ = args[2].StringValue() + } + + list1 := parseStringList(list1Str, delimiter) + list2 := parseStringList(list2Str, delimiter) + + // Create a set from list2 + set := make(map[string]bool) + for _, item := range list2 { + if item != "" { + set[item] = true + } + } + + // Check if all items from list1 are in list2 + for _, item := range list1 { + if item != "" && !set[item] { + return NewBoolValue(false) + } + } + + return NewBoolValue(true) +} + +// builtinStringListRegexpMember checks if pattern matches any element in string list +func builtinStringListRegexpMember(args []Value) Value { + if len(args) < 2 || len(args) > 4 { + return NewErrorValue() + } + + if args[0].IsError() || args[1].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() || args[1].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsString() || !args[1].IsString() { + return NewErrorValue() + } + + pattern, _ := args[0].StringValue() + listStr, _ := args[1].StringValue() + delimiter := "," + options := "" + + // Parse optional arguments + if len(args) >= 3 { + if args[2].IsError() { + return NewErrorValue() + } + if !args[2].IsUndefined() { + if !args[2].IsString() { + return NewErrorValue() + } + delimiter, _ = args[2].StringValue() + } + } + + if len(args) == 4 { + if args[3].IsError() { + return NewErrorValue() + } + if !args[3].IsUndefined() { + if !args[3].IsString() { + return NewErrorValue() + } + options, _ = args[3].StringValue() + } + } + + // Build regex flags + var flags string + if strings.ContainsAny(options, "iI") { + flags += "(?i)" + } + if strings.ContainsAny(options, "mM") { + flags += "(?m)" + } + if strings.ContainsAny(options, "sS") { + flags += "(?s)" + } + + fullPattern := flags + pattern + re, err := regexp.Compile(fullPattern) + if err != nil { + return NewErrorValue() + } + + parts := parseStringList(listStr, delimiter) + for _, part := range parts { + if part != "" && re.MatchString(part) { + return NewBoolValue(true) + } + } + + return NewBoolValue(false) +} + +// builtinRegexpMember checks if pattern matches any string in a list +func builtinRegexpMember(args []Value) Value { + if len(args) < 2 || len(args) > 3 { + return NewErrorValue() + } + + if args[0].IsError() || args[1].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() || args[1].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsString() || !args[1].IsList() { + return NewErrorValue() + } + + pattern, _ := args[0].StringValue() + list, _ := args[1].ListValue() + options := "" + + if len(args) == 3 { + if args[2].IsError() { + return NewErrorValue() + } + if !args[2].IsUndefined() { + if !args[2].IsString() { + return NewErrorValue() + } + options, _ = args[2].StringValue() + } + } + + // Build regex flags + var flags string + if strings.ContainsAny(options, "iI") { + flags += "(?i)" + } + if strings.ContainsAny(options, "mM") { + flags += "(?m)" + } + if strings.ContainsAny(options, "sS") { + flags += "(?s)" + } + + fullPattern := flags + pattern + re, err := regexp.Compile(fullPattern) + if err != nil { + return NewErrorValue() + } + + for _, item := range list { + if item.IsString() { + str, _ := item.StringValue() + if re.MatchString(str) { + return NewBoolValue(true) + } + } + } + + return NewBoolValue(false) +} + +// builtinRegexps performs regex substitution and returns the result +func builtinRegexps(args []Value) Value { + if len(args) < 3 || len(args) > 4 { + return NewErrorValue() + } + + if args[0].IsError() || args[1].IsError() || args[2].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() || args[1].IsUndefined() || args[2].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsString() || !args[1].IsString() || !args[2].IsString() { + return NewErrorValue() + } + + pattern, _ := args[0].StringValue() + target, _ := args[1].StringValue() + substitute, _ := args[2].StringValue() + options := "" + + if len(args) == 4 { + if args[3].IsError() { + return NewErrorValue() + } + if !args[3].IsUndefined() { + if !args[3].IsString() { + return NewErrorValue() + } + options, _ = args[3].StringValue() + } + } + + // Build regex flags + var flags string + if strings.ContainsAny(options, "iI") { + flags += "(?i)" + } + if strings.ContainsAny(options, "mM") { + flags += "(?m)" + } + if strings.ContainsAny(options, "sS") { + flags += "(?s)" + } + + fullPattern := flags + pattern + re, err := regexp.Compile(fullPattern) + if err != nil { + return NewErrorValue() + } + + result := re.ReplaceAllString(target, substitute) + return NewStringValue(result) +} + +// builtinReplace replaces first match of pattern in target +func builtinReplace(args []Value) Value { + if len(args) < 3 || len(args) > 4 { + return NewErrorValue() + } + + if args[0].IsError() || args[1].IsError() || args[2].IsError() { + return NewErrorValue() + } + if args[0].IsUndefined() || args[1].IsUndefined() || args[2].IsUndefined() { + return NewUndefinedValue() + } + if !args[0].IsString() || !args[1].IsString() || !args[2].IsString() { + return NewErrorValue() + } + + pattern, _ := args[0].StringValue() + target, _ := args[1].StringValue() + substitute, _ := args[2].StringValue() + options := "" + + if len(args) == 4 { + if args[3].IsError() { + return NewErrorValue() + } + if !args[3].IsUndefined() { + if !args[3].IsString() { + return NewErrorValue() + } + options, _ = args[3].StringValue() + } + } + + // Build regex flags + var flags string + if strings.ContainsAny(options, "iI") { + flags += "(?i)" + } + if strings.ContainsAny(options, "mM") { + flags += "(?m)" + } + if strings.ContainsAny(options, "sS") { + flags += "(?s)" + } + + fullPattern := flags + pattern + re, err := regexp.Compile(fullPattern) + if err != nil { + return NewErrorValue() + } + + // Replace only first occurrence + loc := re.FindStringIndex(target) + if loc != nil { + result := target[:loc[0]] + substitute + target[loc[1]:] + return NewStringValue(result) + } + + return NewStringValue(target) +} + +// builtinReplaceAll replaces all matches of pattern in target +func builtinReplaceAll(args []Value) Value { + // Same as regexps - replaces all occurrences + return builtinRegexps(args) +} diff --git a/classad/functions_new_test.go b/classad/functions_new_test.go index 5d2969d..3927f67 100644 --- a/classad/functions_new_test.go +++ b/classad/functions_new_test.go @@ -70,6 +70,350 @@ func TestStringListMember(t *testing.T) { } } +func TestStringListIMember(t *testing.T) { + tests := []struct { + name string + expr string + expected bool + }{ + { + name: "case insensitive match lowercase", + expr: `stringListIMember("apple", "Apple,Banana,Cherry")`, + expected: true, + }, + { + name: "case insensitive match uppercase", + expr: `stringListIMember("BANANA", "apple,banana,cherry")`, + expected: true, + }, + { + name: "case insensitive match mixed", + expr: `stringListIMember("BaNaNa", "apple,banana,cherry")`, + expected: true, + }, + { + name: "no match", + expr: `stringListIMember("grape", "apple,banana,cherry")`, + expected: false, + }, + { + name: "match with spaces", + expr: `stringListIMember("APPLE", "apple, banana, cherry")`, + expected: true, + }, + { + name: "empty list", + expr: `stringListIMember("apple", "")`, + expected: false, + }, + { + name: "single element match", + expr: `stringListIMember("APPLE", "apple")`, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ad, err := Parse("[test = " + tt.expr + "]") + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + val := ad.EvaluateAttr("test") + if !val.IsBool() { + t.Fatalf("Expected bool, got %v", val.Type()) + } + result, _ := val.BoolValue() + if result != tt.expected { + t.Errorf("Expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestUnparse(t *testing.T) { + tests := []struct { + name string + classad string + attr string + expected string + }{ + { + name: "simple integer", + classad: `[x = 42; result = unparse(x)]`, + attr: "result", + expected: "42", + }, + { + name: "arithmetic expression", + classad: `[x = 3; y = x + 5; result = unparse(y)]`, + attr: "result", + expected: "(x + 5)", + }, + { + name: "string literal", + classad: `[msg = "hello"; result = unparse(msg)]`, + attr: "result", + expected: `"hello"`, + }, + { + name: "boolean expression", + classad: `[x = 10; cond = x > 5; result = unparse(cond)]`, + attr: "result", + expected: "(x > 5)", + }, + { + name: "function call", + classad: `[name = "John"; upper = toUpper(name); result = unparse(upper)]`, + attr: "result", + expected: "toUpper(name)", + }, + { + name: "conditional expression", + classad: `[x = 10; val = x > 5 ? x : 0; result = unparse(val)]`, + attr: "result", + expected: "((x > 5) ? x : 0)", + }, + { + name: "list literal", + classad: `[nums = {1, 2, 3}; result = unparse(nums)]`, + attr: "result", + expected: "{1, 2, 3}", + }, + { + name: "nested record", + classad: `[rec = [a = 1; b = 2]; result = unparse(rec)]`, + attr: "result", + expected: "[a = 1; b = 2]", + }, + { + name: "attribute reference", + classad: `[x = 5; y = x * 2; result = unparse(y)]`, + attr: "result", + expected: "(x * 2)", + }, + { + name: "complex expression", + classad: `[x = 10; y = 20; z = (x + y) * 2; result = unparse(z)]`, + attr: "result", + expected: "((x + y) * 2)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ad, err := Parse(tt.classad) + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + val := ad.EvaluateAttr(tt.attr) + if !val.IsString() { + t.Fatalf("Expected string, got %v", val.Type()) + } + result, _ := val.StringValue() + if result != tt.expected { + t.Errorf("Expected %q, got %q", tt.expected, result) + } + }) + } + + // Test undefined attribute + t.Run("undefined attribute", func(t *testing.T) { + ad, err := Parse(`[x = 5; result = unparse(missing)]`) + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + val := ad.EvaluateAttr("result") + if !val.IsUndefined() { + t.Errorf("Expected undefined, got %v", val.Type()) + } + }) + + // Test with scoped references + t.Run("MY scope", func(t *testing.T) { + ad, err := Parse(`[x = 10; result = unparse(MY.x)]`) + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + val := ad.EvaluateAttr("result") + if !val.IsString() { + t.Fatalf("Expected string, got %v", val.Type()) + } + result, _ := val.StringValue() + if result != "10" { + t.Errorf("Expected %q, got %q", "10", result) + } + }) +} + +func TestEval(t *testing.T) { + tests := []struct { + name string + classad string + attr string + expected interface{} + expectError bool + expectUndef bool + }{ + { + name: "simple integer expression", + classad: `[result = eval("5 + 3")]`, + attr: "result", + expected: int64(8), + }, + { + name: "string concatenation", + classad: `[result = eval("strcat(\"hello\", \" \", \"world\")")]`, + attr: "result", + expected: "hello world", + }, + { + name: "attribute reference", + classad: `[x = 10; result = eval("x * 2")]`, + attr: "result", + expected: int64(20), + }, + { + name: "boolean expression", + classad: `[x = 15; result = eval("x > 10")]`, + attr: "result", + expected: true, + }, + { + name: "function call in eval", + classad: `[name = "world"; result = eval("strcat(\"hello \", name)")]`, + attr: "result", + expected: "hello world", + }, + { + name: "conditional expression", + classad: `[x = 5; result = eval("x > 3 ? \"yes\" : \"no\"")]`, + attr: "result", + expected: "yes", + }, + { + name: "list expression", + classad: `[result = eval("{1, 2, 3}")]`, + attr: "result", + expected: []int{1, 2, 3}, + }, + { + name: "dynamic attribute name construction", + classad: `[slot1 = 100; slot2 = 200; id = 1; attrname = strcat("slot", string(id)); result = eval(attrname)]`, + attr: "result", + expected: int64(100), + }, + { + name: "nested eval", + classad: `[x = 5; expr = "x + 10"; result = eval(expr)]`, + attr: "result", + expected: int64(15), + }, + { + name: "arithmetic with multiple attributes", + classad: `[a = 10; b = 20; c = 30; result = eval("a + b + c")]`, + attr: "result", + expected: int64(60), + }, + { + name: "invalid expression string", + classad: `[result = eval("this is not valid")]`, + attr: "result", + expectError: true, + }, + { + name: "empty string", + classad: `[result = eval("")]`, + attr: "result", + expectError: true, + }, + { + name: "real number arithmetic", + classad: `[x = 2.5; result = eval("x * 4.0")]`, + attr: "result", + expected: 10.0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ad, err := Parse(tt.classad) + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + val := ad.EvaluateAttr(tt.attr) + + if tt.expectError { + if !val.IsError() { + t.Errorf("Expected error, got %v", val.Type()) + } + return + } + + if tt.expectUndef { + if !val.IsUndefined() { + t.Errorf("Expected undefined, got %v", val.Type()) + } + return + } + + // Check the type and value based on expected + switch exp := tt.expected.(type) { + case int64: + if !val.IsInteger() { + t.Fatalf("Expected integer, got %v", val.Type()) + } + result, _ := val.IntValue() + if result != exp { + t.Errorf("Expected %d, got %d", exp, result) + } + case float64: + if !val.IsReal() { + t.Fatalf("Expected real, got %v", val.Type()) + } + result, _ := val.RealValue() + if result != exp { + t.Errorf("Expected %f, got %f", exp, result) + } + case string: + if !val.IsString() { + t.Fatalf("Expected string, got %v", val.Type()) + } + result, _ := val.StringValue() + if result != exp { + t.Errorf("Expected %q, got %q", exp, result) + } + case bool: + if !val.IsBool() { + t.Fatalf("Expected bool, got %v", val.Type()) + } + result, _ := val.BoolValue() + if result != exp { + t.Errorf("Expected %v, got %v", exp, result) + } + case []int: + if !val.IsList() { + t.Fatalf("Expected list, got %v", val.Type()) + } + list, _ := val.ListValue() + if len(list) != len(exp) { + t.Errorf("Expected list length %d, got %d", len(exp), len(list)) + } + for i, expectedVal := range exp { + if !list[i].IsInteger() { + t.Errorf("Element %d: expected integer, got %v", i, list[i].Type()) + continue + } + actualVal, _ := list[i].IntValue() + if actualVal != int64(expectedVal) { + t.Errorf("Element %d: expected %d, got %d", i, expectedVal, actualVal) + } + } + } + }) + } +} + func TestRegexp(t *testing.T) { tests := []struct { name string @@ -250,3 +594,1370 @@ func TestIfThenElse(t *testing.T) { }) } } + +// TestBuiltinString tests the string() conversion function +func TestBuiltinString(t *testing.T) { + tests := []struct { + name string + classad string + expr string + expected string + }{ + { + name: "integer to string", + classad: "[]", + expr: "string(42)", + expected: "42", + }, + { + name: "real to string", + classad: "[]", + expr: "string(3.14)", + expected: "3.14", + }, + { + name: "boolean true to string", + classad: "[]", + expr: "string(true)", + expected: "true", + }, + { + name: "boolean false to string", + classad: "[]", + expr: "string(false)", + expected: "false", + }, + { + name: "already string", + classad: "[]", + expr: `string("hello")`, + expected: "hello", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ad, err := Parse(tt.classad) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + expr, err := ParseExpr(tt.expr) + if err != nil { + t.Fatalf("ParseExpr failed: %v", err) + } + + val := ad.EvaluateExprWithTarget(expr, nil) + if val.IsError() { + t.Fatalf("Expected string value, got ERROR") + } + if val.IsUndefined() { + t.Fatalf("Expected string value, got UNDEFINED") + } + + str, err := val.StringValue() + if err != nil { + t.Fatalf("StringValue() error: %v", err) + } + if str != tt.expected { + t.Errorf("string() = %q, want %q", str, tt.expected) + } + }) + } +} + +// TestBuiltinBool tests the bool() conversion function +func TestBuiltinBool(t *testing.T) { + tests := []struct { + name string + classad string + expr string + expected bool + isError bool + }{ + { + name: "string true", + classad: "[]", + expr: `bool("true")`, + expected: true, + }, + { + name: "string false", + classad: "[]", + expr: `bool("false")`, + expected: false, + }, + { + name: "integer 1", + classad: "[]", + expr: "bool(1)", + expected: true, + }, + { + name: "integer 0", + classad: "[]", + expr: "bool(0)", + expected: false, + }, + { + name: "integer non-zero", + classad: "[]", + expr: "bool(42)", + expected: true, + }, + { + name: "real non-zero", + classad: "[]", + expr: "bool(3.14)", + expected: true, + }, + { + name: "real zero", + classad: "[]", + expr: "bool(0.0)", + expected: false, + }, + { + name: "invalid string", + classad: "[]", + expr: `bool("invalid")`, + isError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ad, err := Parse(tt.classad) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + expr, err := ParseExpr(tt.expr) + if err != nil { + t.Fatalf("ParseExpr failed: %v", err) + } + + val := ad.EvaluateExprWithTarget(expr, nil) + + if tt.isError { + if !val.IsError() { + t.Errorf("Expected ERROR, got %v", val) + } + return + } + + if val.IsError() { + t.Fatalf("Expected boolean value, got ERROR") + } + if val.IsUndefined() { + t.Fatalf("Expected boolean value, got UNDEFINED") + } + + boolVal, err := val.BoolValue() + if err != nil { + t.Fatalf("BoolValue() error: %v", err) + } + if boolVal != tt.expected { + t.Errorf("bool() = %v, want %v", boolVal, tt.expected) + } + }) + } +} + +// TestBuiltinPow tests the pow() function +func TestBuiltinPow(t *testing.T) { + tests := []struct { + name string + classad string + expr string + expected interface{} // int64 or float64 + }{ + { + name: "integer power positive", + classad: "[]", + expr: "pow(2, 3)", + expected: int64(8), + }, + { + name: "integer power zero", + classad: "[]", + expr: "pow(5, 0)", + expected: int64(1), + }, + { + name: "integer power negative", + classad: "[]", + expr: "pow(2, -2)", + expected: 0.25, + }, + { + name: "real base", + classad: "[]", + expr: "pow(2.0, 3)", + expected: 8.0, + }, + { + name: "real exponent", + classad: "[]", + expr: "pow(4, 0.5)", + expected: 2.0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ad, err := Parse(tt.classad) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + expr, err := ParseExpr(tt.expr) + if err != nil { + t.Fatalf("ParseExpr failed: %v", err) + } + + val := ad.EvaluateExprWithTarget(expr, nil) + if val.IsError() || val.IsUndefined() { + t.Fatalf("Expected numeric value, got %v", val) + } + + switch exp := tt.expected.(type) { + case int64: + if !val.IsInteger() { + t.Errorf("Expected integer, got %v", val.Type()) + } + intVal, _ := val.IntValue() + if intVal != exp { + t.Errorf("pow() = %d, want %d", intVal, exp) + } + case float64: + if !val.IsReal() { + t.Errorf("Expected real, got %v", val.Type()) + } + realVal, _ := val.RealValue() + if realVal != exp { + t.Errorf("pow() = %f, want %f", realVal, exp) + } + } + }) + } +} + +// TestBuiltinSum tests the sum() function +func TestBuiltinSum(t *testing.T) { + tests := []struct { + name string + classad string + expr string + expected interface{} // int64 or float64 + }{ + { + name: "integer list", + classad: "[]", + expr: "sum({1, 2, 3, 4})", + expected: int64(10), + }, + { + name: "real list", + classad: "[]", + expr: "sum({1.5, 2.5, 3.0})", + expected: 7.0, + }, + { + name: "mixed list", + classad: "[]", + expr: "sum({1, 2.5, 3})", + expected: 6.5, + }, + { + name: "empty list", + classad: "[]", + expr: "sum({})", + expected: int64(0), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ad, err := Parse(tt.classad) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + expr, err := ParseExpr(tt.expr) + if err != nil { + t.Fatalf("ParseExpr failed: %v", err) + } + + val := ad.EvaluateExprWithTarget(expr, nil) + if val.IsError() || val.IsUndefined() { + t.Fatalf("Expected numeric value, got %v", val) + } + + switch exp := tt.expected.(type) { + case int64: + intVal, _ := val.IntValue() + if intVal != exp { + t.Errorf("sum() = %d, want %d", intVal, exp) + } + case float64: + realVal, _ := val.RealValue() + if realVal != exp { + t.Errorf("sum() = %f, want %f", realVal, exp) + } + } + }) + } +} + +func TestBuiltinJoin(t *testing.T) { + tests := []struct { + name string + expr string + expected string + }{ + {"join with separator and varargs", `join(",", "a", "b", "c")`, "a,b,c"}, + {"join with list", `join(",", {"hello", "world"})`, "hello,world"}, + {"join no separator", `join({"a", "b", "c"})`, "abc"}, + {"join mixed types", `join("-", 1, 2, 3)`, "1-2-3"}, + {"join empty sep", `join("", "x", "y")`, "xy"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ad, err := Parse("[test = " + tt.expr + "]") + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + val := ad.EvaluateAttr("test") + if val.IsError() { + t.Errorf("join() returned ERROR") + } else if !val.IsString() { + t.Errorf("join() did not return string, got %v", val.Type()) + } else { + str, _ := val.StringValue() + if str != tt.expected { + t.Errorf("join() = %q, want %q", str, tt.expected) + } + } + }) + } +} + +func TestBuiltinSplit(t *testing.T) { + tests := []struct { + name string + expr string + expected []string + }{ + {"split whitespace", `split("one two three")`, []string{"one", "two", "three"}}, + {"split custom delim", `split("a,b,c", ",")`, []string{"a", "b", "c"}}, + {"split multiple delims", `split("a,b;c", ",;")`, []string{"a", "b", "c"}}, + {"split empty", `split("")`, []string{}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ad, err := Parse("[test = " + tt.expr + "]") + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + val := ad.EvaluateAttr("test") + if val.IsError() { + t.Errorf("split() returned ERROR") + } else if !val.IsList() { + t.Errorf("split() did not return list, got %v", val.Type()) + } else { + list, _ := val.ListValue() + if len(list) != len(tt.expected) { + t.Errorf("split() length = %d, want %d", len(list), len(tt.expected)) + } else { + for i, item := range list { + if !item.IsString() { + t.Errorf("split()[%d] is not string", i) + } else { + str, _ := item.StringValue() + if str != tt.expected[i] { + t.Errorf("split()[%d] = %q, want %q", i, str, tt.expected[i]) + } + } + } + } + } + }) + } +} + +func TestBuiltinSplitUserName(t *testing.T) { + tests := []struct { + name string + expr string + expected []string + }{ + {"with domain", `splitUserName("alice@example.com")`, []string{"alice", "example.com"}}, + {"no domain", `splitUserName("bob")`, []string{"bob", ""}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ad, err := Parse("[test = " + tt.expr + "]") + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + val := ad.EvaluateAttr("test") + if val.IsError() { + t.Errorf("splitUserName() returned ERROR") + } else if !val.IsList() { + t.Errorf("splitUserName() did not return list, got %v", val.Type()) + } else { + list, _ := val.ListValue() + if len(list) != 2 { + t.Errorf("splitUserName() length = %d, want 2", len(list)) + } else { + for i := 0; i < 2; i++ { + if !list[i].IsString() { + t.Errorf("splitUserName()[%d] is not string", i) + } else { + str, _ := list[i].StringValue() + if str != tt.expected[i] { + t.Errorf("splitUserName()[%d] = %q, want %q", i, str, tt.expected[i]) + } + } + } + } + } + }) + } +} + +func TestBuiltinStrcmp(t *testing.T) { + tests := []struct { + name string + expr string + expected int64 + }{ + {"equal", `strcmp("abc", "abc")`, 0}, + {"less", `strcmp("abc", "def")`, -1}, + {"greater", `strcmp("def", "abc")`, 1}, + {"case matters", `strcmp("ABC", "abc")`, -1}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ad, err := Parse("[test = " + tt.expr + "]") + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + val := ad.EvaluateAttr("test") + if val.IsError() { + t.Errorf("strcmp() returned ERROR") + } else if !val.IsInteger() { + t.Errorf("strcmp() did not return integer, got %v", val.Type()) + } else { + result, _ := val.IntValue() + // Compare signs only + if (result < 0) != (tt.expected < 0) || (result > 0) != (tt.expected > 0) || (result == 0) != (tt.expected == 0) { + t.Errorf("strcmp() = %d, want %d", result, tt.expected) + } + } + }) + } +} + +func TestBuiltinStricmp(t *testing.T) { + tests := []struct { + name string + expr string + expected int64 + }{ + {"equal", `stricmp("abc", "ABC")`, 0}, + {"less", `stricmp("ABC", "DEF")`, -1}, + {"greater", `stricmp("DEF", "ABC")`, 1}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ad, err := Parse("[test = " + tt.expr + "]") + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + val := ad.EvaluateAttr("test") + if val.IsError() { + t.Errorf("stricmp() returned ERROR") + } else if !val.IsInteger() { + t.Errorf("stricmp() did not return integer, got %v", val.Type()) + } else { + result, _ := val.IntValue() + // Compare signs only + if (result < 0) != (tt.expected < 0) || (result > 0) != (tt.expected > 0) || (result == 0) != (tt.expected == 0) { + t.Errorf("stricmp() = %d, want %d", result, tt.expected) + } + } + }) + } +} + +func TestBuiltinVersioncmp(t *testing.T) { + tests := []struct { + name string + expr string + expected int64 + }{ + {"equal", `versioncmp("1.2.3", "1.2.3")`, 0}, + {"less major", `versioncmp("1.2.3", "2.0.0")`, -1}, + {"greater minor", `versioncmp("1.3.0", "1.2.9")`, 1}, + {"numeric vs text", `versioncmp("1.10", "1.9")`, 1}, + {"different lengths", `versioncmp("1.2", "1.2.0")`, -1}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ad, err := Parse("[test = " + tt.expr + "]") + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + val := ad.EvaluateAttr("test") + if val.IsError() { + t.Errorf("versioncmp() returned ERROR") + } else if !val.IsInteger() { + t.Errorf("versioncmp() did not return integer, got %v", val.Type()) + } else { + result, _ := val.IntValue() + // Compare signs only + if (result < 0) != (tt.expected < 0) || (result > 0) != (tt.expected > 0) || (result == 0) != (tt.expected == 0) { + t.Errorf("versioncmp() = %d, want %d", result, tt.expected) + } + } + }) + } +} + +func TestBuiltinVersionComparisonFunctions(t *testing.T) { + tests := []struct { + name string + expr string + expected bool + }{ + {"version_gt true", `version_gt("2.0", "1.9")`, true}, + {"version_gt false", `version_gt("1.9", "2.0")`, false}, + {"version_ge equal", `version_ge("1.5", "1.5")`, true}, + {"version_lt true", `version_lt("1.0", "2.0")`, true}, + {"version_le equal", `version_le("3.0", "3.0")`, true}, + {"version_eq true", `version_eq("1.2.3", "1.2.3")`, true}, + {"version_in_range true", `version_in_range("1.5", "1.0", "2.0")`, true}, + {"version_in_range false", `version_in_range("2.5", "1.0", "2.0")`, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ad, err := Parse("[test = " + tt.expr + "]") + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + val := ad.EvaluateAttr("test") + if val.IsError() { + t.Errorf("version function returned ERROR") + } else if !val.IsBool() { + t.Errorf("version function did not return boolean, got %v", val.Type()) + } else { + result, _ := val.BoolValue() + if result != tt.expected { + t.Errorf("version function = %v, want %v", result, tt.expected) + } + } + }) + } +} + +func TestBuiltinFormatTime(t *testing.T) { + tests := []struct { + name string + expr string + check func(string) bool + }{ + {"default format", `formatTime(1234567890)`, func(s string) bool { return len(s) > 10 }}, + {"year only", `formatTime(1234567890, "%Y")`, func(s string) bool { return s == "2009" }}, + {"date", `formatTime(1234567890, "%Y-%m-%d")`, func(s string) bool { return s == "2009-02-13" }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ad, err := Parse("[test = " + tt.expr + "]") + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + val := ad.EvaluateAttr("test") + if val.IsError() { + t.Errorf("formatTime() returned ERROR") + } else if !val.IsString() { + t.Errorf("formatTime() did not return string, got %v", val.Type()) + } else { + str, _ := val.StringValue() + if !tt.check(str) { + t.Errorf("formatTime() = %q, check failed", str) + } + } + }) + } +} + +func TestBuiltinInterval(t *testing.T) { + tests := []struct { + name string + expr string + expected string + }{ + {"seconds only", `interval(45)`, "0:45"}, + {"minutes", `interval(125)`, "2:05"}, + {"hours", `interval(3665)`, "1:01:05"}, + {"days", `interval(90125)`, "1+01:02:05"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ad, err := Parse("[test = " + tt.expr + "]") + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + val := ad.EvaluateAttr("test") + if val.IsError() { + t.Errorf("interval() returned ERROR") + } else if !val.IsString() { + t.Errorf("interval() did not return string, got %v", val.Type()) + } else { + str, _ := val.StringValue() + if str != tt.expected { + t.Errorf("interval() = %q, want %q", str, tt.expected) + } + } + }) + } +} + +func TestBuiltinIdenticalMember(t *testing.T) { + tests := []struct { + name string + expr string + expected bool + }{ + {"found integer", `identicalMember(2, {1, 2, 3})`, true}, + {"not found", `identicalMember(4, {1, 2, 3})`, false}, + {"found string", `identicalMember("b", {"a", "b", "c"})`, true}, + {"type mismatch", `identicalMember(2, {"1", "2", "3"})`, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ad, err := Parse("[test = " + tt.expr + "]") + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + val := ad.EvaluateAttr("test") + if val.IsError() { + t.Errorf("identicalMember() returned ERROR") + } else if !val.IsBool() { + t.Errorf("identicalMember() did not return boolean, got %v", val.Type()) + } else { + result, _ := val.BoolValue() + if result != tt.expected { + t.Errorf("identicalMember() = %v, want %v", result, tt.expected) + } + } + }) + } +} + +func TestAnyCompare(t *testing.T) { + tests := []struct { + name string + op string + list []Value + target Value + expected bool + }{ + { + name: "any greater than", + op: ">", + list: []Value{NewIntValue(1), NewIntValue(5), NewIntValue(3)}, + target: NewIntValue(4), + expected: true, + }, + { + name: "any less than", + op: "<", + list: []Value{NewIntValue(10), NewIntValue(5), NewIntValue(8)}, + target: NewIntValue(7), + expected: true, + }, + { + name: "any equals", + op: "==", + list: []Value{NewIntValue(1), NewIntValue(2), NewIntValue(3)}, + target: NewIntValue(2), + expected: true, + }, + { + name: "none match", + op: ">", + list: []Value{NewIntValue(1), NewIntValue(2), NewIntValue(3)}, + target: NewIntValue(5), + expected: false, + }, + { + name: "string comparison", + op: "==", + list: []Value{NewStringValue("a"), NewStringValue("b"), NewStringValue("c")}, + target: NewStringValue("b"), + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := []Value{NewStringValue(tt.op), NewListValue(tt.list), tt.target} + val := builtinAnyCompare(args) + if !val.IsBool() { + t.Errorf("anyCompare() did not return boolean, got %v", val.Type()) + } else { + result, _ := val.BoolValue() + if result != tt.expected { + t.Errorf("anyCompare() = %v, want %v", result, tt.expected) + } + } + }) + } +} + +func TestAllCompare(t *testing.T) { + tests := []struct { + name string + op string + list []Value + target Value + expected bool + }{ + { + name: "all greater than", + op: ">", + list: []Value{NewIntValue(5), NewIntValue(6), NewIntValue(7)}, + target: NewIntValue(4), + expected: true, + }, + { + name: "all less than", + op: "<", + list: []Value{NewIntValue(1), NewIntValue(2), NewIntValue(3)}, + target: NewIntValue(5), + expected: true, + }, + { + name: "not all match", + op: ">", + list: []Value{NewIntValue(5), NewIntValue(3), NewIntValue(7)}, + target: NewIntValue(4), + expected: false, + }, + { + name: "empty list", + op: ">", + list: []Value{}, + target: NewIntValue(5), + expected: true, // vacuously true + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := []Value{NewStringValue(tt.op), NewListValue(tt.list), tt.target} + val := builtinAllCompare(args) + if !val.IsBool() { + t.Errorf("allCompare() did not return boolean, got %v", val.Type()) + } else { + result, _ := val.BoolValue() + if result != tt.expected { + t.Errorf("allCompare() = %v, want %v", result, tt.expected) + } + } + }) + } +} + +func TestStringListSize(t *testing.T) { + tests := []struct { + name string + listStr string + delimiter string + expected int64 + }{ + { + name: "comma separated", + listStr: "a,b,c,d", + expected: 4, + }, + { + name: "semicolon separated", + listStr: "x;y;z", + delimiter: ";", + expected: 3, + }, + { + name: "with spaces", + listStr: "one, two, three", + expected: 3, + }, + { + name: "empty string", + listStr: "", + expected: 0, + }, + { + name: "single item", + listStr: "solo", + expected: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var args []Value + if tt.delimiter != "" { + args = []Value{NewStringValue(tt.listStr), NewStringValue(tt.delimiter)} + } else { + args = []Value{NewStringValue(tt.listStr)} + } + + val := builtinStringListSize(args) + if !val.IsInteger() { + t.Errorf("stringListSize() did not return integer, got %v", val.Type()) + } else { + result, _ := val.IntValue() + if result != tt.expected { + t.Errorf("stringListSize() = %v, want %v", result, tt.expected) + } + } + }) + } +} + +func TestStringListSum(t *testing.T) { + tests := []struct { + name string + listStr string + expected float64 + isInt bool + }{ + { + name: "integers", + listStr: "1,2,3,4", + expected: 10, + isInt: true, + }, + { + name: "reals", + listStr: "1.5,2.5,3.0", + expected: 7.0, + isInt: false, + }, + { + name: "mixed", + listStr: "1,2.5,3", + expected: 6.5, + isInt: false, + }, + { + name: "with spaces", + listStr: "10, 20, 30", + expected: 60, + isInt: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := []Value{NewStringValue(tt.listStr)} + val := builtinStringListSum(args) + if tt.isInt && !val.IsInteger() { + t.Errorf("stringListSum() expected int, got %v", val.Type()) + } else if !tt.isInt && !val.IsReal() { + t.Errorf("stringListSum() expected real, got %v", val.Type()) + } else { + result, _ := val.NumberValue() + if result != tt.expected { + t.Errorf("stringListSum() = %v, want %v", result, tt.expected) + } + } + }) + } +} + +func TestStringListAvg(t *testing.T) { + tests := []struct { + name string + listStr string + expected float64 + }{ + { + name: "integers", + listStr: "2,4,6,8", + expected: 5.0, + }, + { + name: "reals", + listStr: "1.0,2.0,3.0", + expected: 2.0, + }, + { + name: "single value", + listStr: "42", + expected: 42.0, + }, + { + name: "empty list", + listStr: "", + expected: 0.0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := []Value{NewStringValue(tt.listStr)} + val := builtinStringListAvg(args) + if !val.IsReal() { + t.Errorf("stringListAvg() did not return real, got %v", val.Type()) + } else { + result, _ := val.RealValue() + if result != tt.expected { + t.Errorf("stringListAvg() = %v, want %v", result, tt.expected) + } + } + }) + } +} + +func TestStringListMin(t *testing.T) { + tests := []struct { + name string + listStr string + expected float64 + isInt bool + isUndefined bool + }{ + { + name: "integers", + listStr: "5,2,8,1,9", + expected: 1, + isInt: true, + }, + { + name: "reals", + listStr: "5.5,2.1,8.9", + expected: 2.1, + isInt: false, + }, + { + name: "empty list", + listStr: "", + isUndefined: true, + }, + { + name: "negative numbers", + listStr: "5,-3,2", + expected: -3, + isInt: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := []Value{NewStringValue(tt.listStr)} + val := builtinStringListMin(args) + if tt.isUndefined { + if !val.IsUndefined() { + t.Errorf("stringListMin() expected undefined, got %v", val.Type()) + } + } else if tt.isInt && !val.IsInteger() { + t.Errorf("stringListMin() expected int, got %v", val.Type()) + } else if !tt.isInt && !val.IsReal() { + t.Errorf("stringListMin() expected real, got %v", val.Type()) + } else if !tt.isUndefined { + result, _ := val.NumberValue() + if result != tt.expected { + t.Errorf("stringListMin() = %v, want %v", result, tt.expected) + } + } + }) + } +} + +func TestStringListMax(t *testing.T) { + tests := []struct { + name string + listStr string + expected float64 + isInt bool + isUndefined bool + }{ + { + name: "integers", + listStr: "5,2,8,1,9", + expected: 9, + isInt: true, + }, + { + name: "reals", + listStr: "5.5,2.1,8.9", + expected: 8.9, + isInt: false, + }, + { + name: "empty list", + listStr: "", + isUndefined: true, + }, + { + name: "negative numbers", + listStr: "-5,-3,-2", + expected: -2, + isInt: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := []Value{NewStringValue(tt.listStr)} + val := builtinStringListMax(args) + if tt.isUndefined { + if !val.IsUndefined() { + t.Errorf("stringListMax() expected undefined, got %v", val.Type()) + } + } else if tt.isInt && !val.IsInteger() { + t.Errorf("stringListMax() expected int, got %v", val.Type()) + } else if !tt.isInt && !val.IsReal() { + t.Errorf("stringListMax() expected real, got %v", val.Type()) + } else if !tt.isUndefined { + result, _ := val.NumberValue() + if result != tt.expected { + t.Errorf("stringListMax() = %v, want %v", result, tt.expected) + } + } + }) + } +} + +func TestStringListsIntersect(t *testing.T) { + tests := []struct { + name string + list1 string + list2 string + expected bool + }{ + { + name: "common elements", + list1: "a,b,c", + list2: "c,d,e", + expected: true, + }, + { + name: "no common elements", + list1: "a,b,c", + list2: "x,y,z", + expected: false, + }, + { + name: "multiple common", + list1: "1,2,3,4", + list2: "3,4,5,6", + expected: true, + }, + { + name: "empty list1", + list1: "", + list2: "a,b,c", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := []Value{NewStringValue(tt.list1), NewStringValue(tt.list2)} + val := builtinStringListsIntersect(args) + if !val.IsBool() { + t.Errorf("stringListsIntersect() did not return boolean, got %v", val.Type()) + } else { + result, _ := val.BoolValue() + if result != tt.expected { + t.Errorf("stringListsIntersect() = %v, want %v", result, tt.expected) + } + } + }) + } +} + +func TestStringListSubsetMatch(t *testing.T) { + tests := []struct { + name string + list1 string + list2 string + expected bool + }{ + { + name: "is subset", + list1: "a,b", + list2: "a,b,c,d", + expected: true, + }, + { + name: "not subset", + list1: "a,b,x", + list2: "a,b,c,d", + expected: false, + }, + { + name: "equal lists", + list1: "a,b,c", + list2: "a,b,c", + expected: true, + }, + { + name: "empty subset", + list1: "", + list2: "a,b,c", + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := []Value{NewStringValue(tt.list1), NewStringValue(tt.list2)} + val := builtinStringListSubsetMatch(args) + if !val.IsBool() { + t.Errorf("stringListSubsetMatch() did not return boolean, got %v", val.Type()) + } else { + result, _ := val.BoolValue() + if result != tt.expected { + t.Errorf("stringListSubsetMatch() = %v, want %v", result, tt.expected) + } + } + }) + } +} + +func TestStringListRegexpMember(t *testing.T) { + tests := []struct { + name string + pattern string + listStr string + expected bool + }{ + { + name: "match found", + pattern: "^foo", + listStr: "bar,foobar,baz", + expected: true, + }, + { + name: "no match", + pattern: "^xyz", + listStr: "foo,bar,baz", + expected: false, + }, + { + name: "case insensitive", + pattern: "FOO", + listStr: "bar,foo,baz", + expected: false, // no case-insensitive flag + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := []Value{NewStringValue(tt.pattern), NewStringValue(tt.listStr)} + val := builtinStringListRegexpMember(args) + if !val.IsBool() { + t.Errorf("stringListRegexpMember() did not return boolean, got %v", val.Type()) + } else { + result, _ := val.BoolValue() + if result != tt.expected { + t.Errorf("stringListRegexpMember() = %v, want %v", result, tt.expected) + } + } + }) + } +} + +func TestRegexpMember(t *testing.T) { + tests := []struct { + name string + pattern string + list []Value + expected bool + }{ + { + name: "match found", + pattern: "^foo", + list: []Value{NewStringValue("bar"), NewStringValue("foobar"), NewStringValue("baz")}, + expected: true, + }, + { + name: "no match", + pattern: "^xyz", + list: []Value{NewStringValue("foo"), NewStringValue("bar"), NewStringValue("baz")}, + expected: false, + }, + { + name: "partial match", + pattern: "test", + list: []Value{NewStringValue("testing"), NewStringValue("foo"), NewStringValue("bar")}, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := []Value{NewStringValue(tt.pattern), NewListValue(tt.list)} + val := builtinRegexpMember(args) + if !val.IsBool() { + t.Errorf("regexpMember() did not return boolean, got %v", val.Type()) + } else { + result, _ := val.BoolValue() + if result != tt.expected { + t.Errorf("regexpMember() = %v, want %v", result, tt.expected) + } + } + }) + } +} + +func TestRegexps(t *testing.T) { + tests := []struct { + name string + pattern string + target string + subst string + expected string + }{ + { + name: "simple replace", + pattern: "foo", + target: "foo bar foo", + subst: "baz", + expected: "baz bar baz", + }, + { + name: "regex pattern", + pattern: "\\d+", + target: "test123and456", + subst: "X", + expected: "testXandX", + }, + { + name: "no match", + pattern: "xyz", + target: "hello world", + subst: "replacement", + expected: "hello world", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := []Value{NewStringValue(tt.pattern), NewStringValue(tt.target), NewStringValue(tt.subst)} + val := builtinRegexps(args) + if !val.IsString() { + t.Errorf("regexps() did not return string, got %v", val.Type()) + } else { + result, _ := val.StringValue() + if result != tt.expected { + t.Errorf("regexps() = %q, want %q", result, tt.expected) + } + } + }) + } +} + +func TestReplace(t *testing.T) { + tests := []struct { + name string + pattern string + target string + subst string + expected string + }{ + { + name: "replace first", + pattern: "foo", + target: "foo bar foo", + subst: "baz", + expected: "baz bar foo", + }, + { + name: "regex pattern", + pattern: "\\d+", + target: "test123and456", + subst: "X", + expected: "testXand456", + }, + { + name: "no match", + pattern: "xyz", + target: "hello world", + subst: "replacement", + expected: "hello world", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := []Value{NewStringValue(tt.pattern), NewStringValue(tt.target), NewStringValue(tt.subst)} + val := builtinReplace(args) + if !val.IsString() { + t.Errorf("replace() did not return string, got %v", val.Type()) + } else { + result, _ := val.StringValue() + if result != tt.expected { + t.Errorf("replace() = %q, want %q", result, tt.expected) + } + } + }) + } +} + +func TestReplaceAll(t *testing.T) { + tests := []struct { + name string + pattern string + target string + subst string + expected string + }{ + { + name: "replace all", + pattern: "foo", + target: "foo bar foo", + subst: "baz", + expected: "baz bar baz", + }, + { + name: "regex pattern", + pattern: "\\d+", + target: "test123and456end", + subst: "X", + expected: "testXandXend", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := []Value{NewStringValue(tt.pattern), NewStringValue(tt.target), NewStringValue(tt.subst)} + val := builtinReplaceAll(args) + if !val.IsString() { + t.Errorf("replaceAll() did not return string, got %v", val.Type()) + } else { + result, _ := val.StringValue() + if result != tt.expected { + t.Errorf("replaceAll() = %q, want %q", result, tt.expected) + } + } + }) + } +} diff --git a/examples/newfunctions_demo/main.go b/examples/newfunctions_demo/main.go new file mode 100644 index 0000000..c6dd3b0 --- /dev/null +++ b/examples/newfunctions_demo/main.go @@ -0,0 +1,197 @@ +package main + +import ( + "fmt" + "log" + + "github.com/PelicanPlatform/classad/classad" +) + +func main() { + fmt.Println("=== New HTCondor ClassAd Functions Demo ===") + fmt.Println() + + // List comparison functions + fmt.Println("1. List Comparison Functions") + fmt.Println("-----------------------------") + + ad1 := `[ + numbers = {1, 5, 3, 7, 2}; + threshold = 4; + any_gt = anyCompare(">", numbers, threshold); + all_gt = allCompare(">", numbers, threshold); + ]` + + classAd1, err := classad.Parse(ad1) + if err != nil { + log.Fatal(err) + } + + anyGt := classAd1.EvaluateAttr("any_gt") + allGt := classAd1.EvaluateAttr("all_gt") + fmt.Printf(" anyCompare(\">\", {1, 5, 3, 7, 2}, 4) = %v\n", anyGt) + fmt.Printf(" allCompare(\">\", {1, 5, 3, 7, 2}, 4) = %v\n\n", allGt) + + // StringList functions + fmt.Println("2. StringList Functions") + fmt.Println("-----------------------") + + ad2 := `[ + list1 = "10,20,30,40,50"; + list2 = "apple,banana,cherry"; + size = stringListSize(list1); + sum = stringListSum(list1); + avg = stringListAvg(list1); + min = stringListMin(list1); + max = stringListMax(list1); + has_apple = stringListIMember("APPLE", list2); + ]` + + classAd2, err := classad.Parse(ad2) + if err != nil { + log.Fatal(err) + } + + size := classAd2.EvaluateAttr("size") + sum := classAd2.EvaluateAttr("sum") + avg := classAd2.EvaluateAttr("avg") + min := classAd2.EvaluateAttr("min") + max := classAd2.EvaluateAttr("max") + hasApple := classAd2.EvaluateAttr("has_apple") + + fmt.Printf(" stringListSize(\"10,20,30,40,50\") = %v\n", size) + fmt.Printf(" stringListSum(\"10,20,30,40,50\") = %v\n", sum) + fmt.Printf(" stringListAvg(\"10,20,30,40,50\") = %v\n", avg) + fmt.Printf(" stringListMin(\"10,20,30,40,50\") = %v\n", min) + fmt.Printf(" stringListMax(\"10,20,30,40,50\") = %v\n", max) + fmt.Printf(" stringListIMember(\"APPLE\", \"apple,banana,cherry\") = %v (case-insensitive)\n\n", hasApple) + + // StringList set operations + fmt.Println("3. StringList Set Operations") + fmt.Println("-----------------------------") + + ad3 := `[ + set1 = "red,green,blue"; + set2 = "blue,yellow,purple"; + set3 = "red,green"; + intersect = stringListsIntersect(set1, set2); + subset = stringListSubsetMatch(set3, set1); + ]` + + classAd3, err := classad.Parse(ad3) + if err != nil { + log.Fatal(err) + } + + intersect := classAd3.EvaluateAttr("intersect") + subset := classAd3.EvaluateAttr("subset") + + fmt.Printf(" stringListsIntersect(\"red,green,blue\", \"blue,yellow,purple\") = %v\n", intersect) + fmt.Printf(" stringListSubsetMatch(\"red,green\", \"red,green,blue\") = %v\n\n", subset) + + // Regex functions + fmt.Println("4. Regular Expression Functions") + fmt.Println("--------------------------------") + + ad4 := `[ + pattern = "^test"; + list = {"testing", "foo", "test123"}; + text = "hello123world456"; + match = regexpMember(pattern, list); + replaced = regexps("\\d+", text, "X"); + replaceFirst = replace("\\d+", text, "X"); + ]` + + classAd4, err := classad.Parse(ad4) + if err != nil { + log.Fatal(err) + } + + match := classAd4.EvaluateAttr("match") + replaced := classAd4.EvaluateAttr("replaced") + replaceFirst := classAd4.EvaluateAttr("replaceFirst") + + fmt.Printf(" regexpMember(\"^test\", {\"testing\", \"foo\", \"test123\"}) = %v\n", match) + fmt.Printf(" regexps(\"\\\\d+\", \"hello123world456\", \"X\") = %v\n", replaced) + fmt.Printf(" replace(\"\\\\d+\", \"hello123world456\", \"X\") = %v\n\n", replaceFirst) + + // StringList regex + fmt.Println("5. StringList Regular Expression") + fmt.Println("---------------------------------") + + ad5 := `[ + csvList = "test1,foo,test2,bar"; + hasTest = stringListRegexpMember("^test", csvList); + ]` + + classAd5, err := classad.Parse(ad5) + if err != nil { + log.Fatal(err) + } + + hasTest := classAd5.EvaluateAttr("hasTest") + fmt.Printf(" stringListRegexpMember(\"^test\", \"test1,foo,test2,bar\") = %v\n\n", hasTest) + + // unparse() function + fmt.Println("6. Expression Introspection") + fmt.Println("----------------------------") + + ad6 := `[ + x = 10; + y = 20; + sum = x + y; + product = x * y; + condition = x > 5 ? "yes" : "no"; + sum_str = unparse(sum); + product_str = unparse(product); + condition_str = unparse(condition); + ]` + + classAd6, err := classad.Parse(ad6) + if err != nil { + log.Fatal(err) + } + + sumStr := classAd6.EvaluateAttr("sum_str") + productStr := classAd6.EvaluateAttr("product_str") + conditionStr := classAd6.EvaluateAttr("condition_str") + + fmt.Printf(" unparse(sum) = %v\n", sumStr) + fmt.Printf(" unparse(product) = %v\n", productStr) + fmt.Printf(" unparse(condition) = %v\n\n", conditionStr) + + // eval() function + fmt.Println("7. Dynamic Expression Evaluation") + fmt.Println("---------------------------------") + + ad7 := `[ + x = 10; + y = 20; + expr1 = "x + y"; + expr2 = "x * y"; + expr3 = "x > 5 ? 100 : 0"; + result1 = eval(expr1); + result2 = eval(expr2); + result3 = eval(expr3); + slot5 = 500; + slotId = 5; + dynamicAttr = eval(strcat("slot", string(slotId))); + ]` + + classAd7, err := classad.Parse(ad7) + if err != nil { + log.Fatal(err) + } + + result1 := classAd7.EvaluateAttr("result1") + result2 := classAd7.EvaluateAttr("result2") + result3 := classAd7.EvaluateAttr("result3") + dynamicAttr := classAd7.EvaluateAttr("dynamicAttr") + + fmt.Printf(" eval(\"x + y\") where x=10, y=20 = %v\n", result1) + fmt.Printf(" eval(\"x * y\") where x=10, y=20 = %v\n", result2) + fmt.Printf(" eval(\"x > 5 ? 100 : 0\") where x=10 = %v\n", result3) + fmt.Printf(" eval(strcat(\"slot\", string(5))) where slot5=500 = %v\n\n", dynamicAttr) + + fmt.Println("=== All 19 new functions working! ===") +}