Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement numeric conversion. #89

Merged
merged 1 commit into from
Nov 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions builtins/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ func PopulateEnvironment(env *env.Environment) {
env.Set("<", &primitive.Procedure{F: ltFn, Help: helpMap["<"], Args: []primitive.Symbol{primitive.Symbol("a"), primitive.Symbol("b")}})
env.Set("=", &primitive.Procedure{F: equalsFn, Help: helpMap["="], Args: []primitive.Symbol{primitive.Symbol("arg1"), primitive.Symbol("arg2 .. argN")}})
env.Set("arch", &primitive.Procedure{F: archFn, Help: helpMap["arch"]})
env.Set("base", &primitive.Procedure{F: baseFn, Help: helpMap["base"], Args: []primitive.Symbol{primitive.Symbol("number"), primitive.Symbol("base")}})
env.Set("car", &primitive.Procedure{F: carFn, Help: helpMap["car"], Args: []primitive.Symbol{primitive.Symbol("list")}})
env.Set("cdr", &primitive.Procedure{F: cdrFn, Help: helpMap["cdr"], Args: []primitive.Symbol{primitive.Symbol("list")}})
env.Set("char=", &primitive.Procedure{F: charEqualsFn, Help: helpMap["char="], Args: []primitive.Symbol{primitive.Symbol("a"), primitive.Symbol("b")}})
Expand Down Expand Up @@ -131,7 +132,8 @@ func PopulateEnvironment(env *env.Environment) {
env.Set("ms", &primitive.Procedure{F: msFn, Help: helpMap["ms"]})
env.Set("nil?", &primitive.Procedure{F: nilFn, Help: helpMap["nil?"], Args: []primitive.Symbol{primitive.Symbol("object")}})
env.Set("now", &primitive.Procedure{F: nowFn, Help: helpMap["now"]})
env.Set("nth", &primitive.Procedure{F: nthFn, Help: helpMap["nth"],Args: []primitive.Symbol{primitive.Symbol("list"), primitive.Symbol("offset")}})
env.Set("nth", &primitive.Procedure{F: nthFn, Help: helpMap["nth"], Args: []primitive.Symbol{primitive.Symbol("list"), primitive.Symbol("offset")}})
env.Set("number", &primitive.Procedure{F: numberFn, Help: helpMap["number"], Args: []primitive.Symbol{primitive.Symbol("str")}})
env.Set("ord", &primitive.Procedure{F: ordFn, Help: helpMap["ord"], Args: []primitive.Symbol{primitive.Symbol("char")}})
env.Set("os", &primitive.Procedure{F: osFn, Help: helpMap["os"]})
env.Set("print", &primitive.Procedure{F: printFn, Help: helpMap["print"], Args: []primitive.Symbol{primitive.Symbol("arg1..argN")}})
Expand All @@ -154,6 +156,27 @@ func archFn(env *env.Environment, args []primitive.Primitive) primitive.Primitiv
return primitive.String(runtime.GOARCH)
}

// baseFn implements (base)
func baseFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive {
if len(args) != 2 {
return primitive.ArityError()
}

// Get the value
n, ok := args[0].(primitive.Number)
if !ok {
return primitive.Error("argument not a number")
}

// Get the base
base, ok2 := args[1].(primitive.Number)
if !ok2 {
return primitive.Error("argument not a number")
}

return primitive.String(strconv.FormatInt(int64(n), int(base)))
}

// carFn implements "car"
func carFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive {

Expand Down Expand Up @@ -1173,7 +1196,6 @@ func nowFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive
// nthFn is the implementation of `(nth..)`
func nthFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive {


// We need two arguments.
if len(args) != 2 {
return primitive.ArityError()
Expand Down Expand Up @@ -1201,6 +1223,40 @@ func nthFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive
return primitive.Error("out of bounds")
}

// numberFn is the implementation of (number ..)
func numberFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive {
// we only accept a single parameter
if len(args) != 1 {
return primitive.ArityError()
}

// The argument must be a string
str, ok := args[0].(primitive.String)
if !ok {
return primitive.Error("argument not a string")
}

// Lower-case so our prefix-matching works
s := strings.ToLower(str.ToString())
if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0b") {

// If so then parse as an integer
n, err := strconv.ParseInt(s, 0, 32)
if err == nil {
return primitive.Number(n)
}
}

// Is it a number?
f, err := strconv.ParseFloat(s, 64)
if err == nil {

return primitive.Number(f)
}

return primitive.Error(fmt.Sprintf("failed to convert %s to number", args[0].ToString()))
}

// ordFn is the implementation of (ord ..)
func ordFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive {

Expand Down
138 changes: 133 additions & 5 deletions builtins/builtins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,67 @@ func TestArch(t *testing.T) {
}
}

// Test (base
func TestBase(t *testing.T) {

// No arguments
out := baseFn(ENV, []primitive.Primitive{})

// Will lead to an error
e, ok := out.(primitive.Error)
if !ok {
t.Fatalf("expected error, got %v", out)
}
if e != primitive.ArityError() {
t.Fatalf("got error, but wrong one %v", out)
}

// Arguments must be numbers
out = baseFn(ENV, []primitive.Primitive{
primitive.Number(3),
primitive.String("foo"),
})

// Will lead to an error
e, ok = out.(primitive.Error)
if !ok {
t.Fatalf("expected error, got %v", out)
}
if !strings.Contains(string(e), "not a number") {
t.Fatalf("got error, but wrong one %v", out)
}

// Arguments must be numbers
out = baseFn(ENV, []primitive.Primitive{
primitive.String("foo"),
primitive.Number(3),
})

// Will lead to an error
e, ok = out.(primitive.Error)
if !ok {
t.Fatalf("expected error, got %v", out)
}
if !strings.Contains(string(e), "not a number") {
t.Fatalf("got error, but wrong one %v", out)
}

// Valid result
result := baseFn(ENV, []primitive.Primitive{
primitive.Number(255),
primitive.Number(2),
})

// Will lead to an error
r, ok2 := result.(primitive.String)
if !ok2 {
t.Fatalf("expected string, got %v", result)
}
if !strings.Contains(string(r), "11111111") {
t.Fatalf("got string, but wrong one %v", r)
}
}

// Test (car
func TestCar(t *testing.T) {

Expand Down Expand Up @@ -2561,7 +2622,6 @@ func TestNth(t *testing.T) {
// No arguments
out := nthFn(ENV, []primitive.Primitive{})


// Will lead to an error
e, ok := out.(primitive.Error)
if !ok {
Expand All @@ -2587,7 +2647,6 @@ func TestNth(t *testing.T) {
t.Fatalf("got error, but wrong one %v", out)
}


// Not a number
out = nthFn(ENV, []primitive.Primitive{
primitive.List{},
Expand All @@ -2602,13 +2661,11 @@ func TestNth(t *testing.T) {
t.Fatalf("got error, but wrong one %v", out)
}


// bound checking
var l primitive.List
l = append(l, primitive.String("one"))
l = append(l, primitive.String("two"))


// negative offset
out = nthFn(ENV, []primitive.Primitive{
l,
Expand Down Expand Up @@ -2639,7 +2696,6 @@ func TestNth(t *testing.T) {
t.Fatalf("got error, but wrong one %v", out)
}


// valid access
str := nthFn(ENV, []primitive.Primitive{
l,
Expand Down Expand Up @@ -2670,6 +2726,78 @@ func TestNth(t *testing.T) {

}

func TestNumber(t *testing.T) {

// No arguments
out := numberFn(ENV, []primitive.Primitive{})

// Will lead to an error
e, ok := out.(primitive.Error)
if !ok {
t.Fatalf("expected error, got %v", out)
}
if e != primitive.ArityError() {
t.Fatalf("got error, but wrong one:%s", out)
}

// Must be a string.
out = numberFn(ENV, []primitive.Primitive{
primitive.Number(3),
})

// Will lead to an error
e, ok = out.(primitive.Error)
if !ok {
t.Fatalf("expected error, got %v", out)
}
if !strings.Contains(string(e), "not a string") {
t.Fatalf("got error, but wrong one %v", out)
}

// Type for table-driven tests
type Input struct {
Inp string
Out int
}

// Trivial test-cases with base10, base2, and base16
tests := []Input{
Input{"3", 3},
Input{"0b1111", 15},
Input{"0xff", 255},
}

for _, tst := range tests {

res := numberFn(ENV, []primitive.Primitive{
primitive.String(tst.Inp),
})

// Will lead to a number
r, ok2 := res.(primitive.Number)
if !ok2 {
t.Fatalf("expected number, got %v", res)
}
if int(r) != tst.Out {
t.Fatalf("got %d, not %d", int(r), tst.Out)
}
}

// Failure to convert a bogus number
out = numberFn(ENV, []primitive.Primitive{
primitive.String("3.3.3.3"),
})

// Will lead to an error
e, ok = out.(primitive.Error)
if !ok {
t.Fatalf("expected error, got %v", out)
}
if !strings.Contains(string(e), "failed to convert") {
t.Fatalf("got error, but wrong one %v", out)
}
}

func TestOrd(t *testing.T) {

// no arguments
Expand Down
23 changes: 23 additions & 0 deletions builtins/help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ arch returns a simple string describing the architecture the current host is run
See also: (os)
Example : (print (arch))
%%
base

Convert the given number into a string representation in the specified base.

See also: number, sprintf, str

Example: (print (base 255 2)) ; base two is binary
Example: (print (base 255 16)) ; base 16 is hexadecimal
%%
car
car returns the first item from the specified list.
%%
Expand Down Expand Up @@ -241,6 +250,16 @@ NOTE: The offset starts from 0, to access the first item.

Example: (print (nth '( 1 2 3 ) 0 ) )
%%
number

Number will convert the given string to a number object, and supports
hexadecimal, binary, and base-ten values.

Example: (print (number "0xffed"))
Example: (print (number "0b1011"))

See also: base, str
%%
ord

ord returns the ASCII code for the character provided as the first input.
Expand Down Expand Up @@ -329,10 +348,14 @@ When a format string is used it can contain the following strings:

See also: print
Example: (sprintf "Today is %s" (weekday))
Example: (sprintf "31 in binary is %08b" 31)
%%
str

str converts the parameter supplied to a string, and returns it.

Example: (print (str 3))
See also: base, number
%%
time

Expand Down