Repository: diegoholiveira/jsonlogic
Pull request #106 (2025-02-26 22:15:00)

create the javascript package to encapsulate specific JS behavior


Patch:
diff --git a/comp.go b/comp.go
index 4d461b2..842b03c 100644
--- a/comp.go
+++ b/comp.go
@@ -2,17 +2,9 @@ package jsonlogic
 
 import (
 	"reflect"
-)
-
-type undefinedType struct{}
 
-// at simulate undefined in javascript
-func at(values []any, index int) any {
-	if index >= 0 && index < len(values) {
-		return values[index]
-	}
-	return undefinedType{}
-}
+	"github.com/diegoholiveira/jsonlogic/v3/internal/javascript"
+)
 
 // less reference javascript implementation
 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Less_than#description
@@ -24,7 +16,7 @@ func less(a, b any) bool {
 	}
 
 	// Otherwise the values are compared as numeric values.
-	return toNumberFromAny(b) > toNumberFromAny(a)
+	return javascript.ToNumber(b) > javascript.ToNumber(a)
 }
 
 func hardEquals(a, b any) bool {
@@ -49,5 +41,5 @@ func equals(a, b any) bool {
 		return a == b
 	}
 
-	return toNumberFromAny(a) == toNumberFromAny(b)
+	return javascript.ToNumber(a) == javascript.ToNumber(b)
 }
diff --git a/helpers.go b/helpers.go
index cff98d9..09dc834 100644
--- a/helpers.go
+++ b/helpers.go
@@ -1,10 +1,8 @@
 package jsonlogic
 
 import (
-	"math"
 	"reflect"
 	"strconv"
-	"strings"
 )
 
 func is(obj any, kind reflect.Kind) bool {
@@ -85,50 +83,6 @@ func toNumber(value any) float64 {
 	}
 }
 
-// toNumberFromAny converts various input types to float64.
-//
-// Examples:
-//
-//	toNumberFromAny(42)                             // Returns: 42.0
-//	toNumberFromAny("3.14")                         // Returns: 3.14
-//	toNumberFromAny(true)                           // Returns: 1.0
-//	toNumberFromAny(false)                          // Returns: 0.0
-//	toNumberFromAny([]int{1, 2, 3})                 // Returns: 3.0 (length of slice)
-//	toNumberFromAny(map[string]int{"a": 1, "b": 2}) // Returns: 2.0 (length of map)
-//	toNumberFromAny(nil)                            // Returns: 0.0
-//
-// Note: For unsupported types, it returns 0.0
-func toNumberFromAny(v any) float64 {
-	switch value := v.(type) {
-	case nil:
-		return 0
-	case undefinedType:
-		return math.NaN()
-	case float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
-		return reflect.ValueOf(value).Convert(reflect.TypeOf(float64(0))).Float()
-	case bool: // Boolean values true and false are converted to 1 and 0 respectively.
-		if value {
-			return 1
-		} else {
-			return 0
-		}
-	case string:
-		if strings.TrimSpace(value) == "" {
-			return 0
-		}
-
-		n, err := strconv.ParseFloat(value, 64)
-		switch err {
-		case strconv.ErrRange, nil:
-			return n
-		default:
-			return math.NaN()
-		}
-	default:
-		return math.NaN()
-	}
-}
-
 func toString(value any) string {
 	if isNumber(value) {
 		switch value := value.(type) {
diff --git a/internal/javascript/javascript.go b/internal/javascript/javascript.go
new file mode 100644
index 0000000..5a2fd1e
--- /dev/null
+++ b/internal/javascript/javascript.go
@@ -0,0 +1,69 @@
+package javascript
+
+import (
+	"math"
+	"reflect"
+	"strconv"
+	"strings"
+)
+
+type UndefinedType struct{}
+
+// At returns the element at the specified index in the slice.
+// If index is negative, it counts from the end of the slice.
+// If index is out of bounds, it returns nil.
+//
+// Example:
+//
+//	At([]any{1,2,3}, 1)  // Returns: 2
+//	At([]any{1,2,3}, -1) // Returns: 3
+func At(values []any, index int) any {
+	if index >= 0 && index < len(values) {
+		return values[index]
+	}
+	return UndefinedType{}
+}
+
+// ToNumber converts various input types to float64.
+//
+// Examples:
+//
+//	ToNumber(42)                             // Returns: 42.0
+//	ToNumber("3.14")                         // Returns: 3.14
+//	ToNumber(true)                           // Returns: 1.0
+//	ToNumber(false)                          // Returns: 0.0
+//	ToNumber([]int{1, 2, 3})                 // Returns: 3.0 (length of slice)
+//	ToNumber(map[string]int{"a": 1, "b": 2}) // Returns: 2.0 (length of map)
+//	ToNumber(nil)                            // Returns: 0.0
+//
+// Note: For unsupported types, it returns 0.0
+func ToNumber(v any) float64 {
+	switch value := v.(type) {
+	case nil:
+		return 0
+	case UndefinedType:
+		return math.NaN()
+	case float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
+		return reflect.ValueOf(value).Convert(reflect.TypeOf(float64(0))).Float()
+	case bool: // Boolean values true and false are converted to 1 and 0 respectively.
+		if value {
+			return 1
+		} else {
+			return 0
+		}
+	case string:
+		if strings.TrimSpace(value) == "" {
+			return 0
+		}
+
+		n, err := strconv.ParseFloat(value, 64)
+		switch err {
+		case strconv.ErrRange, nil:
+			return n
+		default:
+			return math.NaN()
+		}
+	default:
+		return math.NaN()
+	}
+}
diff --git a/operation.go b/operation.go
index 4645109..2a16c15 100644
--- a/operation.go
+++ b/operation.go
@@ -1,5 +1,9 @@
 package jsonlogic
 
+import (
+	"github.com/diegoholiveira/jsonlogic/v3/internal/javascript"
+)
+
 // customOperators holds custom operators
 var customOperators = make(map[string]func(values, data any) (result any))
 
@@ -122,35 +126,35 @@ func operation(operator string, values, data any) any {
 	}
 
 	if operator == "<" {
-		return less(at(parsed, 0), at(parsed, 1))
+		return less(javascript.At(parsed, 0), javascript.At(parsed, 1))
 	}
 
 	if operator == ">" {
-		return less(at(parsed, 1), at(parsed, 0))
+		return less(javascript.At(parsed, 1), javascript.At(parsed, 0))
 	}
 
 	if operator == "<=" {
-		return less(parsed[0], parsed[1]) || equals(parsed[0], parsed[1])
+		return less(javascript.At(parsed, 0), javascript.At(parsed, 1)) || equals(javascript.At(parsed, 0), javascript.At(parsed, 1))
 	}
 
 	if operator == ">=" {
-		return less(parsed[1], parsed[0]) || equals(parsed[0], parsed[1])
+		return less(javascript.At(parsed, 1), javascript.At(parsed, 0)) || equals(javascript.At(parsed, 0), javascript.At(parsed, 1))
 	}
 
 	if operator == "===" {
-		return hardEquals(parsed[0], parsed[1])
+		return hardEquals(javascript.At(parsed, 0), javascript.At(parsed, 1))
 	}
 
 	if operator == "!=" {
-		return !equals(parsed[0], parsed[1])
+		return !equals(javascript.At(parsed, 0), javascript.At(parsed, 1))
 	}
 
 	if operator == "!==" {
-		return !hardEquals(parsed[0], parsed[1])
+		return !hardEquals(javascript.At(parsed, 0), javascript.At(parsed, 1))
 	}
 
 	if operator == "==" {
-		return equals(parsed[0], parsed[1])
+		return equals(javascript.At(parsed, 0), javascript.At(parsed, 1))
 	}
 
 	panic(ErrInvalidOperator{


Test patch:
diff --git a/comp_test.go b/comp_test.go
index bf5ee62..65548a0 100644
--- a/comp_test.go
+++ b/comp_test.go
@@ -5,6 +5,8 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+
+	"github.com/diegoholiveira/jsonlogic/v3/internal/javascript"
 )
 
 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Less_than#string_to_string_comparison
@@ -45,8 +47,8 @@ func TestLessCompareBooleanNullUndefinedNan(t *testing.T) {
 	assert.False(t, less(nil, 0))
 	assert.True(t, less(nil, 1))
 
-	assert.False(t, less(undefinedType{}, 3))
-	assert.False(t, less(3, undefinedType{}))
+	assert.False(t, less(javascript.UndefinedType{}, 3))
+	assert.False(t, less(3, javascript.UndefinedType{}))
 
 	assert.False(t, less(3, math.NaN()))
 	assert.False(t, less(math.NaN(), 3))
diff --git a/internal/javascript/javascript_test.go b/internal/javascript/javascript_test.go
new file mode 100644
index 0000000..ab28df5
--- /dev/null
+++ b/internal/javascript/javascript_test.go
@@ -0,0 +1,131 @@
+package javascript
+
+import (
+	"math"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAt(t *testing.T) {
+	tests := []struct {
+		name     string
+		values   []any
+		index    int
+		expected any
+	}{
+		{
+			name:     "valid index",
+			values:   []any{1, "test", true},
+			index:    1,
+			expected: "test",
+		},
+		{
+			name:     "index out of bounds (positive)",
+			values:   []any{1, 2, 3},
+			index:    5,
+			expected: UndefinedType{},
+		},
+		{
+			name:     "index out of bounds (negative)",
+			values:   []any{1, 2, 3},
+			index:    -1,
+			expected: UndefinedType{},
+		},
+		{
+			name:     "empty array",
+			values:   []any{},
+			index:    0,
+			expected: UndefinedType{},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := At(tt.values, tt.index)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestToNumber(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    any
+		expected float64
+		isNaN    bool
+	}{
+		{
+			name:     "nil input",
+			input:    nil,
+			expected: 0,
+		},
+		{
+			name:  "undefined input",
+			input: UndefinedType{},
+			isNaN: true,
+		},
+		{
+			name:     "int input",
+			input:    42,
+			expected: 42,
+		},
+		{
+			name:     "float64 input",
+			input:    3.14,
+			expected: 3.14,
+		},
+		{
+			name:     "true boolean input",
+			input:    true,
+			expected: 1,
+		},
+		{
+			name:     "false boolean input",
+			input:    false,
+			expected: 0,
+		},
+		{
+			name:     "valid numeric string",
+			input:    "123.45",
+			expected: 123.45,
+		},
+		{
+			name:     "empty string",
+			input:    "",
+			expected: 0,
+		},
+		{
+			name:     "whitespace string",
+			input:    "   ",
+			expected: 0,
+		},
+		{
+			name:  "invalid numeric string",
+			input: "not a number",
+			isNaN: true,
+		},
+		{
+			name:  "complex type (map)",
+			input: map[string]int{"a": 1, "b": 2},
+			isNaN: true,
+		},
+		{
+			name:  "complex type (struct)",
+			input: struct{ Name string }{"test"},
+			isNaN: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := ToNumber(tt.input)
+
+			if tt.isNaN {
+				assert.True(t, math.IsNaN(result), "Expected NaN result for %v", tt.input)
+			} else {
+				assert.Equal(t, tt.expected, result)
+			}
+		})
+	}
+}