From e19cd2eb9ef2cedd87ee16e83e04b07f42eed8e4 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Mon, 17 Oct 2022 19:45:00 +0300 Subject: [PATCH] Added /= function. This is a weird one, but it closes #53 by allowing a numerical inequality test to be carried out: * If any argument has been seen before, return false. * If all arguments are unique, return true. This closes #53. --- PRIMITIVES.md | 2 + builtins/builtins.go | 50 ++++++++++++++++- builtins/builtins_test.go | 109 ++++++++++++++++++++++++++++++++++++++ builtins/help.txt | 4 ++ eval/eval_test.go | 9 ++-- 5 files changed, 168 insertions(+), 6 deletions(-) diff --git a/PRIMITIVES.md b/PRIMITIVES.md index 7145830..46bb300 100644 --- a/PRIMITIVES.md +++ b/PRIMITIVES.md @@ -72,6 +72,8 @@ Things you'll find here include: * `/` * Division function. * Note that if only a single value is specified the reciprocal is returned - i.e. "(/ 3)" is equal to "1/3". +* `/=` + * Numerical inequality test, if any argument is the same as another return false, otherwise if all arguments are unique return true. * `<` * Less-than function. * `=` diff --git a/builtins/builtins.go b/builtins/builtins.go index 6bdce66..a13504d 100644 --- a/builtins/builtins.go +++ b/builtins/builtins.go @@ -96,7 +96,8 @@ func PopulateEnvironment(env *env.Environment) { env.Set("*", &primitive.Procedure{F: multiplyFn, Help: helpMap["*"], Args: []primitive.Symbol{primitive.Symbol("N"), primitive.Symbol("arg1..argN")}}) env.Set("+", &primitive.Procedure{F: plusFn, Help: helpMap["+"], Args: []primitive.Symbol{primitive.Symbol("N"), primitive.Symbol("arg1..argN")}}) env.Set("-", &primitive.Procedure{F: minusFn, Help: helpMap["-"], Args: []primitive.Symbol{primitive.Symbol("N"), primitive.Symbol("arg1..argN")}}) - env.Set("/", &primitive.Procedure{F: divideFn, Help: helpMap["-"], Args: []primitive.Symbol{primitive.Symbol("N"), primitive.Symbol("arg1..argN")}}) + env.Set("/", &primitive.Procedure{F: divideFn, Help: helpMap["/"], Args: []primitive.Symbol{primitive.Symbol("N"), primitive.Symbol("arg1..argN")}}) + env.Set("/=", &primitive.Procedure{F: inequalityFn, Help: helpMap["/="], Args: []primitive.Symbol{primitive.Symbol("N"), primitive.Symbol("arg1..argN")}}) 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"]}) @@ -786,6 +787,53 @@ func helpFn(env *env.Environment, args []primitive.Primitive) primitive.Primitiv return primitive.String(str) } +// inequalityFn implements /= +func inequalityFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive { + + // We need at least two arguments + if len(args) < 2 { + return primitive.ArityError() + } + + // First argument must be a number. + nA, ok := args[0].(primitive.Number) + if !ok { + return primitive.Error("argument was not a number") + } + + // Now we'll loop over all other numbers + // + // If we got something that was already seen we can + // terminate early but we don't because it is important + // to also report on failures to validate types - which + // we can't do if we bail. + // + ret := primitive.Bool(true) + + // Keep track of things we've seen here + seen := make(map[float64]bool) + seen[float64(nA)] = true + + for _, i := range args[1:] { + + // check we have a number + nB, ok2 := i.(primitive.Number) + + if !ok2 { + return primitive.Error("argument was not a number") + } + + // Have we seen this? + _, found := seen[float64(nB)] + if found { + ret = primitive.Bool(false) + } + seen[float64(nB)] = true + } + + return ret +} + // (join (1 2 3) func joinFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive { diff --git a/builtins/builtins_test.go b/builtins/builtins_test.go index 520abde..4700559 100644 --- a/builtins/builtins_test.go +++ b/builtins/builtins_test.go @@ -1610,6 +1610,115 @@ func TestHelp(t *testing.T) { } } +// TestInequality tests /= +func TestInequality(t *testing.T) { + + // No arguments + out := inequalityFn(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) + } + + // + // Now a real one: unequal + // + out = inequalityFn(ENV, []primitive.Primitive{ + primitive.Number(9), + primitive.Number(8), + }) + + // Will work + n, ok2 := out.(primitive.Bool) + if !ok2 { + t.Fatalf("expected bool, got %v", out) + } + if n != true { + t.Fatalf("got wrong result") + } + + // + // Now a real one: unequal - but multiple values + // + out = inequalityFn(ENV, []primitive.Primitive{ + primitive.Number(1), + primitive.Number(2), + primitive.Number(3), + primitive.Number(4), + primitive.Number(5), + primitive.Number(6), + }) + + // Will work + n, ok2 = out.(primitive.Bool) + if !ok2 { + t.Fatalf("expected bool, got %v", out) + } + if n != true { + t.Fatalf("got wrong result") + } + + // + // Now a real one: unequal values + // + out = inequalityFn(ENV, []primitive.Primitive{ + primitive.Number(1), + primitive.Number(2), + primitive.Number(2), + primitive.Number(1), + }) + + // Will work + n, ok2 = out.(primitive.Bool) + if !ok2 { + t.Fatalf("expected bool, got %v", out) + } + if n != false { + t.Fatalf("got wrong result") + } + + // + // Now with wrong types - last one is wrong + // + out = inequalityFn(ENV, []primitive.Primitive{ + primitive.Number(1), + primitive.Number(2), + primitive.Number(3), + primitive.Number(4), + primitive.String("5"), + }) + + e, ok = out.(primitive.Error) + if !ok { + t.Fatalf("expected error, got %v", out) + } + if !strings.Contains(string(e), "was not a number") { + t.Fatalf("got error, but wrong one '%v'", e) + } + + // + // Now with wrong types + // + out = inequalityFn(ENV, []primitive.Primitive{ + primitive.String("9"), + primitive.Number(9), + }) + + // Will report an error + e, ok = out.(primitive.Error) + if !ok { + t.Fatalf("expected error, got %v", out) + } + if !strings.Contains(string(e), "was not a number") { + t.Fatalf("got error, but wrong one %v", out) + } +} + // TestJoin tests join func TestJoin(t *testing.T) { diff --git a/builtins/help.txt b/builtins/help.txt index d3ddbb3..cc1e524 100644 --- a/builtins/help.txt +++ b/builtins/help.txt @@ -16,6 +16,10 @@ Multiplies all arguments present with the first number. / Divides all arguments present with the first number. %% +/= +Numerical inequality testing. If any argument is identical +to any other argument return false. Otherwise return true. +%% < Return true if a is less than b. diff --git a/eval/eval_test.go b/eval/eval_test.go index a591048..4ae1326 100644 --- a/eval/eval_test.go +++ b/eval/eval_test.go @@ -187,7 +187,6 @@ a // {"(+ 3)", "3"}, {"(- 3)", "3"}, - {"(/ 3)", "3"}, {"(* 3)", "3"}, // strings @@ -358,12 +357,12 @@ func TestStandardLibrary(t *testing.T) { output: "(1 4 9 16 25)"}, // range - {input: "(range -5 5 1)", output: "(-5 -4 -3 -2 -1 0 1 2 3 4)"}, - {input: "(range 1 11 2)", output: "(1 3 5 7 9)"}, + {input: "(range -5 5 1)", output: "(-5 -4 -3 -2 -1 0 1 2 3 4 5)"}, + {input: "(range 1 11 2)", output: "(1 3 5 7 9 11)"}, // seq/nat - {input: "(seq 10)", output: "(0 1 2 3 4 5 6 7 8 9)"}, - {input: "(nat 10)", output: "(1 2 3 4 5 6 7 8 9)"}, + {input: "(seq 10)", output: "(0 1 2 3 4 5 6 7 8 9 10)"}, + {input: "(nat 10)", output: "(1 2 3 4 5 6 7 8 9 10)"}, {input: "(join (reverse (split \"Steve\" \"\")))", output: "evetS"}, }