From f7a0bb0897210c8f6afbbf9809775bcb0c5db2b3 Mon Sep 17 00:00:00 2001 From: huanghongkai Date: Mon, 26 Jun 2023 21:34:56 +0800 Subject: [PATCH] feat: add sort module to stdlib --- stdlib/source_modules.go | 1 + stdlib/srcmod_sort.tengo | 33 +++++++++++++++ stdlib/stdlib_test.go | 86 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 stdlib/srcmod_sort.tengo diff --git a/stdlib/source_modules.go b/stdlib/source_modules.go index 50e09287..c8bcb943 100644 --- a/stdlib/source_modules.go +++ b/stdlib/source_modules.go @@ -5,4 +5,5 @@ package stdlib // SourceModules are source type standard library modules. var SourceModules = map[string]string{ "enum": "is_enumerable := func(x) {\n return is_array(x) || is_map(x) || is_immutable_array(x) || is_immutable_map(x)\n}\n\nis_array_like := func(x) {\n return is_array(x) || is_immutable_array(x)\n}\n\nexport {\n // all returns true if the given function `fn` evaluates to a truthy value on\n // all of the items in `x`. It returns undefined if `x` is not enumerable.\n all: func(x, fn) {\n if !is_enumerable(x) { return undefined }\n\n for k, v in x {\n if !fn(k, v) { return false }\n }\n\n return true\n },\n // any returns true if the given function `fn` evaluates to a truthy value on\n // any of the items in `x`. It returns undefined if `x` is not enumerable.\n any: func(x, fn) {\n if !is_enumerable(x) { return undefined }\n\n for k, v in x {\n if fn(k, v) { return true }\n }\n\n return false\n },\n // chunk returns an array of elements split into groups the length of size.\n // If `x` can't be split evenly, the final chunk will be the remaining elements.\n // It returns undefined if `x` is not array.\n chunk: func(x, size) {\n if !is_array_like(x) || !size { return undefined }\n\n numElements := len(x)\n if !numElements { return [] }\n\n res := []\n idx := 0\n for idx < numElements {\n res = append(res, x[idx:idx+size])\n idx += size\n }\n\n return res\n },\n // at returns an element at the given index (if `x` is array) or\n // key (if `x` is map). It returns undefined if `x` is not enumerable.\n at: func(x, key) {\n if !is_enumerable(x) { return undefined }\n\n if is_array_like(x) {\n if !is_int(key) { return undefined }\n } else {\n if !is_string(key) { return undefined }\n }\n\n return x[key]\n },\n // each iterates over elements of `x` and invokes `fn` for each element. `fn` is\n // invoked with two arguments: `key` and `value`. `key` is an int index\n // if `x` is array. `key` is a string key if `x` is map. It does not iterate\n // and returns undefined if `x` is not enumerable.\n each: func(x, fn) {\n if !is_enumerable(x) { return undefined }\n\n for k, v in x {\n fn(k, v)\n }\n },\n // filter iterates over elements of `x`, returning an array of all elements `fn`\n // returns truthy for. `fn` is invoked with two arguments: `key` and `value`.\n // `key` is an int index if `x` is array. It returns undefined if `x` is not array.\n filter: func(x, fn) {\n if !is_array_like(x) { return undefined }\n\n dst := []\n for k, v in x {\n if fn(k, v) { dst = append(dst, v) }\n }\n\n return dst\n },\n // find iterates over elements of `x`, returning value of the first element `fn`\n // returns truthy for. `fn` is invoked with two arguments: `key` and `value`.\n // `key` is an int index if `x` is array. `key` is a string key if `x` is map.\n // It returns undefined if `x` is not enumerable.\n find: func(x, fn) {\n if !is_enumerable(x) { return undefined }\n\n for k, v in x {\n if fn(k, v) { return v }\n }\n },\n // find_key iterates over elements of `x`, returning key or index of the first\n // element `fn` returns truthy for. `fn` is invoked with two arguments: `key`\n // and `value`. `key` is an int index if `x` is array. `key` is a string key if\n // `x` is map. It returns undefined if `x` is not enumerable.\n find_key: func(x, fn) {\n if !is_enumerable(x) { return undefined }\n\n for k, v in x {\n if fn(k, v) { return k }\n }\n },\n // map creates an array of values by running each element in `x` through `fn`.\n // `fn` is invoked with two arguments: `key` and `value`. `key` is an int index\n // if `x` is array. `key` is a string key if `x` is map. It returns undefined\n // if `x` is not enumerable.\n map: func(x, fn) {\n if !is_enumerable(x) { return undefined }\n\n dst := []\n for k, v in x {\n dst = append(dst, fn(k, v))\n }\n\n return dst\n },\n // key returns the first argument.\n key: func(k, _) { return k },\n // value returns the second argument.\n value: func(_, v) { return v }\n}\n", + "sort": "_sort := func(arr, left, right, less) {\n if right-left <= 0 {\n return arr\n }\n i := left\n\n for j := left; j < right; j++ {\n if less(j, right) {\n if i != j {\n tmp := arr[i]\n arr[i] = arr[j]\n arr[j] = tmp\n }\n i++\n }\n }\n\n if i != right {\n tmp := arr[i]\n arr[i] = arr[right]\n arr[right] = tmp\n }\n\n _sort(arr, left, i-1, less)\n _sort(arr, i+1, right, less)\n return arr\n}\n\nexport {\n sort: func(arr, less) {\n return _sort(arr, 0, len(arr)-1, less)\n }\n}", } diff --git a/stdlib/srcmod_sort.tengo b/stdlib/srcmod_sort.tengo new file mode 100644 index 00000000..e00a7355 --- /dev/null +++ b/stdlib/srcmod_sort.tengo @@ -0,0 +1,33 @@ +_sort := func(arr, left, right, less) { + if right-left <= 0 { + return arr + } + i := left + + for j := left; j < right; j++ { + if less(j, right) { + if i != j { + tmp := arr[i] + arr[i] = arr[j] + arr[j] = tmp + } + i++ + } + } + + if i != right { + tmp := arr[i] + arr[i] = arr[right] + arr[right] = tmp + } + + _sort(arr, left, i-1, less) + _sort(arr, i+1, right, less) + return arr +} + +export { + sort: func(arr, less) { + return _sort(arr, 0, len(arr)-1, less) + } +} \ No newline at end of file diff --git a/stdlib/stdlib_test.go b/stdlib/stdlib_test.go index ecd59788..2f2cf944 100644 --- a/stdlib/stdlib_test.go +++ b/stdlib/stdlib_test.go @@ -1,7 +1,11 @@ package stdlib_test import ( + "encoding/json" "fmt" + "math/rand" + "sort" + "strings" "testing" "time" @@ -72,6 +76,81 @@ if !is_error(cmd) { } +func TestSortModule(t *testing.T) { + // normal + expect(t, ` +sort := import("sort") +a := [4, 5, 3, 1, 2] +a = sort.sort(a, func(i, j) { + return a[i] < a[j] +}) +out := import("json").encode(a) +`, []byte("[1,2,3,4,5]")) + + // generate random sequences for sorting + rand.Seed(time.Now().UnixNano()) + generateRandomSequence := func() []int { + length := 1 + rand.Intn(1000) + arr := make([]int, length) + for i := 0; i < length; i++ { + arr[i] = rand.Intn(1000000) + } + return arr + } + for i := 0; i < 500; i++ { + seq := generateRandomSequence() + seqBytes, _ := json.Marshal(seq) + sort.Ints(seq) + sortResult, _ := json.Marshal(seq) + expect(t, fmt.Sprintf(` +sort := import("sort") +a := %s +a = sort.sort(a, func(i, j) { + return a[i] < a[j] +}) +out := import("json").encode(a) +`, string(seqBytes)), sortResult) + } + + // less is not a function + expectErr(t, ` +sort := import("sort") +a := [4, 5, 3, 1, 2] +a = sort.sort(a, 0) +out := import("json").encode(a)`, "Runtime Error: not callable: int") + + // arr is not an array + expectErr(t, ` +sort := import("sort") +a := 12345 +a = sort.sort(a, func(i, j) { + return a[i] < a[j] +}) +out := import("json").encode(a)`, "Runtime Error: invalid type for argument 'first' in call to 'builtin-function:len': expected array/s") + + // empty array + expect(t, ` +sort := import("sort") +a := [] +a = sort.sort(a, func(i, j) { + return a[i] < a[j] +}) +out := import("json").encode(a)`, []byte("[]")) + + // sort json + expect(t, ` +sort := import("sort") +a := [{"age": 12, "name": "A"}, {"age": 18, "name": "B"}, {"age": 9, "name": "C"}, {"age": 10, "name": "D"}, {"age": 21, "name": "E"}] +a = sort.sort(a, func(i, j) { + return a[i].age < a[j].age +}) +out := [] +for item in a { + out = append(out, item.name) +} +out = import("json").encode(out)`, []byte(`["C","D","A","B","E"]`)) +} + func TestGetModules(t *testing.T) { mods := stdlib.GetModuleMap() require.Equal(t, 0, mods.Len()) @@ -240,3 +319,10 @@ func expect(t *testing.T, input string, expected interface{}) { require.NotNil(t, v) require.Equal(t, expected, v.Value()) } + +func expectErr(t *testing.T, input string, errMsg string) { + s := tengo.NewScript([]byte(input)) + s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) + _, err := s.Run() + require.True(t, strings.Contains(err.Error(), errMsg)) +}