diff --git a/CHANGELOG.md b/CHANGELOG.md index ee5b7f78..85af36fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - `arr.sort`の処理を非同期的にして高速化 - `arr.flat`,`arr.flat_map`を追加 - `Uri:encode_full`, `Uri:encode_component`, `Uri:decode_full`, `Uri:decode_component`を追加 +- `str.starts_with`,`str.ends_with`を追加 # 0.18.0 - `Core:abort`でプログラムを緊急停止できるように diff --git a/docs/primitive-props.md b/docs/primitive-props.md index 6f66d498..37fb6b05 100644 --- a/docs/primitive-props.md +++ b/docs/primitive-props.md @@ -62,6 +62,20 @@ Core:range(0,2).push(4) //[0,1,2,4] ### @(_v_: str).incl(_keyword_: str): bool 文字列中に _keyword_ が含まれていれば`true`、なければ`false`を返します。 +### @(_v_: str).starts_with(_prefix_: str, _start\_index_?: num): bool +文字列が _prefix_ で始まっていれば`true`、そうでなければ`false`を返します。\ +_prefix_ が空文字列の場合は常に`true`を返します。\ +_start\_index_ が指定されている場合、そのインデックスから始めます。\ +_start\_index_ が`v.len`より大きいか`-v.len`より小さい場合は`false`を返します。\ +_start\_index_ が負の場合は末尾から数えます。 + +### @(_v_: str).ends_with(_suffix_: str, _end\_index_?: num): bool +文字列が _suffix_ で終わっていれば`true`、そうでなければ`false`を返します。\ +_suffix_ が空文字列の場合は常に`true`を返します。\ +_end\_index_ が指定されている場合、そのインデックスの直前を末尾とします。(省略時は`v.len`)\ +_end\_index_ が`v.len`より大きいか`-v.len`より小さい場合は`false`を返します。\ +_end\_index_ が負の場合は末尾から数えます。 + ### @(_v_: str).slice(_begin_: num, _end_: num): str 文字列の _begin_ 番目から _end_ 番目の直前までの部分を取得します。 diff --git a/src/interpreter/primitive-props.ts b/src/interpreter/primitive-props.ts index bfc89e55..2ec7476a 100644 --- a/src/interpreter/primitive-props.ts +++ b/src/interpreter/primitive-props.ts @@ -120,6 +120,37 @@ const PRIMITIVE_PROPS: { return Number.isNaN(res) ? NULL : NUM(res); }), + starts_with: (target: VStr): VFn => FN_NATIVE(async ([prefix, start_index], _opts) => { + assertString(prefix); + if (!prefix.value) { + return TRUE; + } + + if (start_index) assertNumber(start_index); + const raw_index = start_index?.value ?? 0; + if (raw_index < -target.value.length || raw_index > target.value.length) { + return FALSE; + } + const index = (raw_index >= 0) ? raw_index : target.value.length + raw_index; + return target.value.startsWith(prefix.value, index) ? TRUE : FALSE; + }), + + ends_with: (target: VStr): VFn => FN_NATIVE(async ([suffix, end_index], _opts) => { + assertString(suffix); + if (!suffix.value) { + return TRUE; + } + + if (end_index) assertNumber(end_index); + const raw_index = end_index?.value ?? target.value.length; + if (raw_index < -target.value.length || raw_index > target.value.length) { + return FALSE; + } + const index = (raw_index >= 0) ? raw_index : target.value.length + raw_index; + + return target.value.endsWith(suffix.value, index) ? TRUE : FALSE; + }), + pad_start: (target: VStr): VFn => FN_NATIVE(([width, pad], _) => { assertNumber(width); const s = (pad) ? (assertString(pad), pad.value) : ' '; diff --git a/test/index.ts b/test/index.ts index 026603fe..cf5fb90e 100644 --- a/test/index.ts +++ b/test/index.ts @@ -2628,7 +2628,91 @@ describe('primitive props', () => { ); }); - test.concurrent("pad_start", async () => { + test.concurrent('starts_with (no index)', async () => { + const res = await exe(` + let str = "hello" + let empty = "" + <: [ + str.starts_with(""), str.starts_with("hello"), + str.starts_with("he"), str.starts_with("ell"), + empty.starts_with(""), empty.starts_with("he"), + ] + `); + eq(res, ARR([ + TRUE, TRUE, + TRUE, FALSE, + TRUE, FALSE, + ])); + }); + + test.concurrent('starts_with (with index)', async () => { + const res = await exe(` + let str = "hello" + let empty = "" + <: [ + str.starts_with("", 4), str.starts_with("he", 0), + str.starts_with("ll", 2), str.starts_with("lo", 3), + str.starts_with("lo", -2), str.starts_with("hel", -5), + str.starts_with("he", 2), str.starts_with("loa", 3), + str.starts_with("lo", -6), str.starts_with("", -7), + str.starts_with("lo", 6), str.starts_with("", 7), + empty.starts_with("", 2), empty.starts_with("ll", 2), + ] + `); + eq(res, ARR([ + TRUE, TRUE, + TRUE, TRUE, + TRUE, TRUE, + FALSE, FALSE, + FALSE, TRUE, + FALSE, TRUE, + TRUE, FALSE, + ])); + }); + + test.concurrent('ends_with (no index)', async () => { + const res = await exe(` + let str = "hello" + let empty = "" + <: [ + str.ends_with(""), str.ends_with("hello"), + str.ends_with("lo"), str.ends_with("ell"), + empty.ends_with(""), empty.ends_with("he"), + ] + `); + eq(res, ARR([ + TRUE, TRUE, + TRUE, FALSE, + TRUE, FALSE, + ])); + }); + + test.concurrent('ends_with (with index)', async () => { + const res = await exe(` + let str = "hello" + let empty = "" + <: [ + str.ends_with("", 3), str.ends_with("lo", 5), + str.ends_with("ll", 4), str.ends_with("he", 2), + str.ends_with("ll", -1), str.ends_with("he", -3), + str.ends_with("he", 5), str.ends_with("lo", 3), + str.ends_with("lo", -6), str.ends_with("", -7), + str.ends_with("lo", 6), str.ends_with("", 7), + empty.ends_with("", 2), empty.ends_with("ll", 2), + ] + `); + eq(res, ARR([ + TRUE, TRUE, + TRUE, TRUE, + TRUE, TRUE, + FALSE, FALSE, + FALSE, TRUE, + FALSE, TRUE, + TRUE, FALSE, + ])); + }); + + test.concurrent("pad_start", async () => { const res = await exe(` let str = "abc" <: [