diff --git a/CHANGELOG.md b/CHANGELOG.md index 2036b999..f8fba738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - `str.pad_start`,`str.pad_end`を追加 - `arr.insert`,`arr.remove`を追加 - `arr.sort`の処理を非同期的にして高速化 +- `arr.flat`,`arr.flat_map`を追加 # 0.18.0 - `Core:abort`でプログラムを緊急停止できるように diff --git a/docs/primitive-props.md b/docs/primitive-props.md index bf9087eb..6f66d498 100644 --- a/docs/primitive-props.md +++ b/docs/primitive-props.md @@ -196,6 +196,14 @@ _fromIndex_ および _toIndex_ に関する挙動は`arr.slice`に準拠しま `arr.copy`同様シャローコピーであり、配列やオブジェクトの参照は維持されます。 _times_ には0以上の整数値を指定します。それ以外ではエラーになります。 +### @(_v_: arr).flat(_depth_?: num): arr +配列に含まれる配列を _depth_ で指定した深さの階層まで結合した新しい配列を作成します。 +_depth_ には0以上の整数値を指定します。省略時は1になります。 + +### @(_v_: arr).flat_map(_func_: @(_item_: value, _index_: num) { value }): arr +配列の各要素を _func_ の返り値で置き換えた後、1階層平坦化した新しい配列を作成します。 +_func_ は非同期的に呼び出されます。 + ### @(_v_: arr).insert(_index_: num, _item_: value): null **【この操作は配列を書き換えます】** 配列の _index_ の位置に _item_ を挿入します。\ diff --git a/src/interpreter/primitive-props.ts b/src/interpreter/primitive-props.ts index 07b9a08e..bfc89e55 100644 --- a/src/interpreter/primitive-props.ts +++ b/src/interpreter/primitive-props.ts @@ -2,7 +2,7 @@ import { substring, length, indexOf, toArray } from 'stringz'; import { AiScriptRuntimeError } from '../error.js'; import { textEncoder } from '../const.js'; -import { assertArray, assertBoolean, assertFunction, assertNumber, assertString, expectAny, eq } from './util.js'; +import { assertArray, assertBoolean, assertFunction, assertNumber, assertString, expectAny, eq, isArray } from './util.js'; import { ARR, FALSE, FN_NATIVE, NULL, NUM, STR, TRUE } from './value.js'; import type { Value, VArr, VFn, VNum, VStr, VError } from './value.js'; @@ -296,6 +296,39 @@ const PRIMITIVE_PROPS: { } }), + flat: (target: VArr): VFn => FN_NATIVE(async ([depth], opts) => { + depth = depth ?? NUM(1); + assertNumber(depth); + if (!Number.isInteger(depth.value)) throw new AiScriptRuntimeError('arr.flat expected integer, got non-integer'); + if (depth.value < 0) throw new AiScriptRuntimeError('arr.flat expected non-negative number, got negative'); + const flat = (arr: Value[], depth: number, result: Value[]) => { + if (depth === 0) { + result.push(...arr); + return; + } + for (const v of arr) { + if (isArray(v)) { + flat(v.value, depth - 1, result); + } else { + result.push(v); + } + } + }; + const result: Value[] = []; + flat(target.value, depth.value, result); + return ARR(result); + }), + + flat_map: (target: VArr): VFn => FN_NATIVE(async ([fn], opts) => { + assertFunction(fn); + const vals = target.value.map(async (item, i) => { + const result = await opts.call(fn, [item, NUM(i)]); + return isArray(result) ? result.value : result; + }); + const mapped_vals = await Promise.all(vals); + return ARR(mapped_vals.flat()); + }), + every: (target: VArr): VFn => FN_NATIVE(async ([fn], opts) => { assertFunction(fn); for (let i = 0; i < target.value.length; i++) { diff --git a/test/index.ts b/test/index.ts index 0a6bddb7..529e85a5 100644 --- a/test/index.ts +++ b/test/index.ts @@ -2931,6 +2931,49 @@ describe('primitive props', () => { ])); }); + test.concurrent('flat', async () => { + const res = await exe(` + var arr1 = [0, [1], [2, 3], [4, [5, 6]]] + let arr2 = arr1.flat() + let arr3 = arr1.flat(2) + <: [arr1, arr2, arr3] + `); + eq(res, ARR([ + ARR([ + NUM(0), ARR([NUM(1)]), ARR([NUM(2), NUM(3)]), + ARR([NUM(4), ARR([NUM(5), NUM(6)])]) + ]), // target not changed + ARR([ + NUM(0), NUM(1), NUM(2), NUM(3), + NUM(4), ARR([NUM(5), NUM(6)]), + ]), + ARR([ + NUM(0), NUM(1), NUM(2), NUM(3), + NUM(4), NUM(5), NUM(6), + ]), + ])); + }); + + test.concurrent('flat_map', async () => { + const res = await exe(` + let arr1 = [0, 1, 2] + let arr2 = ["a", "b"] + let arr3 = arr1.flat_map(@(x){ arr2.map(@(y){ [x, y] }) }) + <: [arr1, arr3] + `); + eq(res, ARR([ + ARR([NUM(0), NUM(1), NUM(2)]), // target not changed + ARR([ + ARR([NUM(0), STR("a")]), + ARR([NUM(0), STR("b")]), + ARR([NUM(1), STR("a")]), + ARR([NUM(1), STR("b")]), + ARR([NUM(2), STR("a")]), + ARR([NUM(2), STR("b")]), + ]), + ])); + }); + test.concurrent('every', async () => { const res = await exe(` let arr1 = [0, 1, 2, 3]