Skip to content

Commit

Permalink
Implement numeric conversion.
Browse files Browse the repository at this point in the history
This pull-request closes #80, by implementing support for numeric
conversion - decimal to binary/hex, and the other way round.
  • Loading branch information
skx committed Nov 12, 2022
1 parent 4db7a29 commit 6fa397d
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 7 deletions.
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

0 comments on commit 6fa397d

Please sign in to comment.