Skip to content

Commit 19b7d64

Browse files
committed
Add numeric conversion builtins
1 parent 0483bc6 commit 19b7d64

5 files changed

Lines changed: 148 additions & 1 deletion

File tree

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ Goal: reduce host-side boilerplate for common scripting tasks.
285285
- [x] Add regex matching/replacement helpers.
286286
- [x] Add UUID/random identifier utilities with deterministic test hooks.
287287
- [x] Add richer date/time parsing helpers for common layouts.
288-
- [ ] Add safer numeric conversions and clamp/round helpers.
288+
- [x] Add safer numeric conversions and clamp/round helpers.
289289

290290
### Collections and Strings
291291

docs/builtins.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,21 @@ short = random_id(8)
7575
token = random_id()
7676
```
7777

78+
## Numeric Conversion
79+
80+
### `to_int(value)`
81+
82+
Converts `int`, integral `float`, or base-10 numeric `string` values into `int`.
83+
84+
### `to_float(value)`
85+
86+
Converts `int`, `float`, or numeric `string` values into `float`.
87+
88+
```vibe
89+
count = to_int("42")
90+
ratio = to_float("1.25")
91+
```
92+
7893
## JSON
7994

8095
### `JSON.parse(string)`

vibes/builtins.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import (
55
"encoding/json"
66
"fmt"
77
"io"
8+
"math"
89
"reflect"
910
"regexp"
11+
"strconv"
1012
"strings"
1113
"time"
1214
)
@@ -132,6 +134,76 @@ func builtinRandomID(exec *Execution, receiver Value, args []Value, kwargs map[s
132134
return NewString(string(chars)), nil
133135
}
134136

137+
func builtinToInt(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) {
138+
if len(args) != 1 {
139+
return NewNil(), fmt.Errorf("to_int expects a single value argument")
140+
}
141+
if len(kwargs) > 0 {
142+
return NewNil(), fmt.Errorf("to_int does not accept keyword arguments")
143+
}
144+
if !block.IsNil() {
145+
return NewNil(), fmt.Errorf("to_int does not accept blocks")
146+
}
147+
148+
switch args[0].Kind() {
149+
case KindInt:
150+
return args[0], nil
151+
case KindFloat:
152+
f := args[0].Float()
153+
if math.Trunc(f) != f {
154+
return NewNil(), fmt.Errorf("to_int cannot convert non-integer float")
155+
}
156+
n, err := floatToInt64Checked(f, "to_int")
157+
if err != nil {
158+
return NewNil(), err
159+
}
160+
return NewInt(n), nil
161+
case KindString:
162+
s := strings.TrimSpace(args[0].String())
163+
if s == "" {
164+
return NewNil(), fmt.Errorf("to_int expects a numeric string")
165+
}
166+
n, err := strconv.ParseInt(s, 10, 64)
167+
if err != nil {
168+
return NewNil(), fmt.Errorf("to_int expects a base-10 integer string")
169+
}
170+
return NewInt(n), nil
171+
default:
172+
return NewNil(), fmt.Errorf("to_int expects int, float, or string")
173+
}
174+
}
175+
176+
func builtinToFloat(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) {
177+
if len(args) != 1 {
178+
return NewNil(), fmt.Errorf("to_float expects a single value argument")
179+
}
180+
if len(kwargs) > 0 {
181+
return NewNil(), fmt.Errorf("to_float does not accept keyword arguments")
182+
}
183+
if !block.IsNil() {
184+
return NewNil(), fmt.Errorf("to_float does not accept blocks")
185+
}
186+
187+
switch args[0].Kind() {
188+
case KindInt:
189+
return NewFloat(float64(args[0].Int())), nil
190+
case KindFloat:
191+
return args[0], nil
192+
case KindString:
193+
s := strings.TrimSpace(args[0].String())
194+
if s == "" {
195+
return NewNil(), fmt.Errorf("to_float expects a numeric string")
196+
}
197+
f, err := strconv.ParseFloat(s, 64)
198+
if err != nil {
199+
return NewNil(), fmt.Errorf("to_float expects a numeric string")
200+
}
201+
return NewFloat(f), nil
202+
default:
203+
return NewNil(), fmt.Errorf("to_float expects int, float, or string")
204+
}
205+
}
206+
135207
func formatUUIDv4(raw []byte) string {
136208
hexValue := hex.EncodeToString(raw)
137209
return hexValue[0:8] + "-" + hexValue[8:12] + "-" + hexValue[12:16] + "-" + hexValue[16:20] + "-" + hexValue[20:32]

vibes/interpreter.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ func NewEngine(cfg Config) (*Engine, error) {
7777
engine.RegisterZeroArgBuiltin("now", builtinNow)
7878
engine.RegisterZeroArgBuiltin("uuid", builtinUUID)
7979
engine.RegisterBuiltin("random_id", builtinRandomID)
80+
engine.RegisterBuiltin("to_int", builtinToInt)
81+
engine.RegisterBuiltin("to_float", builtinToFloat)
8082
engine.builtins["JSON"] = NewObject(map[string]Value{
8183
"parse": NewBuiltin("JSON.parse", builtinJSONParse),
8284
"stringify": NewBuiltin("JSON.stringify", builtinJSONStringify),

vibes/runtime_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1689,6 +1689,64 @@ func TestTimeParseCommonLayouts(t *testing.T) {
16891689
}
16901690
}
16911691

1692+
func TestNumericConversionBuiltins(t *testing.T) {
1693+
script := compileScript(t, `
1694+
def conversions()
1695+
{
1696+
int_from_int: to_int(5),
1697+
int_from_float: to_int(5.0),
1698+
int_from_string: to_int("42"),
1699+
float_from_int: to_float(5),
1700+
float_from_float: to_float(1.25),
1701+
float_from_string: to_float("2.5")
1702+
}
1703+
end
1704+
1705+
def bad_int_fraction()
1706+
to_int(1.5)
1707+
end
1708+
1709+
def bad_int_string()
1710+
to_int("abc")
1711+
end
1712+
1713+
def bad_float_string()
1714+
to_float("abc")
1715+
end
1716+
`)
1717+
1718+
result := callFunc(t, script, "conversions", nil)
1719+
if result.Kind() != KindHash {
1720+
t.Fatalf("expected hash, got %v", result.Kind())
1721+
}
1722+
got := result.Hash()
1723+
if !got["int_from_int"].Equal(NewInt(5)) || !got["int_from_float"].Equal(NewInt(5)) || !got["int_from_string"].Equal(NewInt(42)) {
1724+
t.Fatalf("to_int conversions mismatch: %#v", got)
1725+
}
1726+
if got["float_from_int"].Kind() != KindFloat || got["float_from_int"].Float() != 5 {
1727+
t.Fatalf("float_from_int mismatch: %v", got["float_from_int"])
1728+
}
1729+
if got["float_from_float"].Kind() != KindFloat || got["float_from_float"].Float() != 1.25 {
1730+
t.Fatalf("float_from_float mismatch: %v", got["float_from_float"])
1731+
}
1732+
if got["float_from_string"].Kind() != KindFloat || got["float_from_string"].Float() != 2.5 {
1733+
t.Fatalf("float_from_string mismatch: %v", got["float_from_string"])
1734+
}
1735+
1736+
_, err := script.Call(context.Background(), "bad_int_fraction", nil, CallOptions{})
1737+
if err == nil || !strings.Contains(err.Error(), "to_int cannot convert non-integer float") {
1738+
t.Fatalf("expected fractional to_int error, got %v", err)
1739+
}
1740+
_, err = script.Call(context.Background(), "bad_int_string", nil, CallOptions{})
1741+
if err == nil || !strings.Contains(err.Error(), "to_int expects a base-10 integer string") {
1742+
t.Fatalf("expected string to_int error, got %v", err)
1743+
}
1744+
_, err = script.Call(context.Background(), "bad_float_string", nil, CallOptions{})
1745+
if err == nil || !strings.Contains(err.Error(), "to_float expects a numeric string") {
1746+
t.Fatalf("expected string to_float error, got %v", err)
1747+
}
1748+
}
1749+
16921750
func TestJSONBuiltins(t *testing.T) {
16931751
script := compileScript(t, `
16941752
def parse_payload()

0 commit comments

Comments
 (0)