From 3bb9a5221ff3e75ef937935a86548f608558c96d Mon Sep 17 00:00:00 2001 From: "Randall C. O'Reilly" Date: Sat, 24 Oct 2026 13:55:15 +0200 Subject: [PATCH] feat(strings): add CutLast Go 1.27 adds strings.CutLast, which the strings override did not provide: its parity ledger classified it as blocked, so any program that referenced it failed to compile with parity-blocked-use. This includes programs that only take its value, such as interpreter symbol tables generated for Go 1.27. Implement CutLast like Cut, cutting at the last instance of sep, and classify it as real since go1.27 so the ledger stays valid on older toolchains. Add unit tests and a compliance test whose output matches native Go. Signed-off-by: Randall C. O'Reilly --- gs/strings/godoc.txt | 1 + gs/strings/parity.json | 2 +- gs/strings/strings.test.ts | 11 ++++++ gs/strings/strings.ts | 9 +++++ tests/tests/strings_cutlast/expected.log | 4 +++ tests/tests/strings_cutlast/index.ts | 0 .../tests/strings_cutlast/strings_cutlast.go | 19 +++++++++++ .../strings_cutlast/strings_cutlast.gs.ts | 34 +++++++++++++++++++ tests/tests/strings_cutlast/tsconfig.json | 25 ++++++++++++++ 9 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 tests/tests/strings_cutlast/expected.log create mode 100644 tests/tests/strings_cutlast/index.ts create mode 100644 tests/tests/strings_cutlast/strings_cutlast.go create mode 100644 tests/tests/strings_cutlast/strings_cutlast.gs.ts create mode 100644 tests/tests/strings_cutlast/tsconfig.json diff --git a/gs/strings/godoc.txt b/gs/strings/godoc.txt index dc47e99aa..34f36d4a2 100644 --- a/gs/strings/godoc.txt +++ b/gs/strings/godoc.txt @@ -12,6 +12,7 @@ func ContainsFunc(s string, f func(rune) bool) bool func ContainsRune(s string, r rune) bool func Count(s, substr string) int func Cut(s, sep string) (before, after string, found bool) +func CutLast(s, sep string) (before, after string, found bool) func CutPrefix(s, prefix string) (after string, found bool) func CutSuffix(s, suffix string) (before string, found bool) func EqualFold(s, t string) bool diff --git a/gs/strings/parity.json b/gs/strings/parity.json index e3761b1ba..e3358eef9 100644 --- a/gs/strings/parity.json +++ b/gs/strings/parity.json @@ -29,7 +29,7 @@ "Cut": { "status": "real" }, - "CutLast": { "status": "blocked", "reason": "stdlib function added in a newer toolchain; not implemented by the override" }, + "CutLast": { "status": "real", "since": "go1.27" }, "CutPrefix": { "status": "real" }, diff --git a/gs/strings/strings.test.ts b/gs/strings/strings.test.ts index 60e208ebb..cd367e102 100644 --- a/gs/strings/strings.test.ts +++ b/gs/strings/strings.test.ts @@ -10,6 +10,7 @@ import { ContainsRune, Count, Cut, + CutLast, CutPrefix, CutSuffix, EqualFold, @@ -128,6 +129,16 @@ describe('strings', () => { }) }) + describe('CutLast', () => { + it('should cut around the last separator', () => { + expect(CutLast('a-b-c', '-')).toEqual(['a-b', 'c', true]) + expect(CutLast('hello', '-')).toEqual(['hello', '', false]) + expect(CutLast('', '-')).toEqual(['', '', false]) + expect(CutLast('a--b', '--')).toEqual(['a', 'b', true]) + expect(CutLast('abc', '')).toEqual(['abc', '', true]) + }) + }) + describe('CutPrefix', () => { it('should cut prefix and report if found', () => { expect(CutPrefix('hello world', 'hello ')).toEqual(['world', true]) diff --git a/gs/strings/strings.ts b/gs/strings/strings.ts index 25dffdb8f..6e9d99208 100644 --- a/gs/strings/strings.ts +++ b/gs/strings/strings.ts @@ -662,6 +662,15 @@ export function Cut(s: string, sep: string): [string, string, boolean] { return [s.slice(0, index), s.slice(index + sep.length), true] } +// CutLast slices s around the last instance of sep, returning the text before and after sep. +export function CutLast(s: string, sep: string): [string, string, boolean] { + const index = s.lastIndexOf(sep) + if (index === -1) { + return [s, '', false] + } + return [s.slice(0, index), s.slice(index + sep.length), true] +} + // CutPrefix returns s without the provided leading prefix string and reports whether it found the prefix. export function CutPrefix(s: string, prefix: string): [string, boolean] { if (s.startsWith(prefix)) { diff --git a/tests/tests/strings_cutlast/expected.log b/tests/tests/strings_cutlast/expected.log new file mode 100644 index 000000000..3447d1c72 --- /dev/null +++ b/tests/tests/strings_cutlast/expected.log @@ -0,0 +1,4 @@ +CutLast: a/b c true +Cut: a b/c true +missing: abc true false +multi: héllo::wörld end true diff --git a/tests/tests/strings_cutlast/index.ts b/tests/tests/strings_cutlast/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/tests/tests/strings_cutlast/strings_cutlast.go b/tests/tests/strings_cutlast/strings_cutlast.go new file mode 100644 index 000000000..c4671a630 --- /dev/null +++ b/tests/tests/strings_cutlast/strings_cutlast.go @@ -0,0 +1,19 @@ +package main + +import "strings" + +func main() { + // CutLast splits around the last separator, like Cut does around the first. + before, after, found := strings.CutLast("a/b/c", "/") + println("CutLast:", before, after, found) + before, after, found = strings.Cut("a/b/c", "/") + println("Cut:", before, after, found) + + // Without the separator, it returns s, "", false. + before, after, found = strings.CutLast("abc", "/") + println("missing:", before, after == "", found) + + // A multi-byte separator and non-ASCII text cut at the last match. + before, after, found = strings.CutLast("héllo::wörld::end", "::") + println("multi:", before, after, found) +} diff --git a/tests/tests/strings_cutlast/strings_cutlast.gs.ts b/tests/tests/strings_cutlast/strings_cutlast.gs.ts new file mode 100644 index 000000000..5879aa4cc --- /dev/null +++ b/tests/tests/strings_cutlast/strings_cutlast.gs.ts @@ -0,0 +1,34 @@ +// Generated file based on strings_cutlast.go +// Updated when compliance tests are re-run, DO NOT EDIT! + +import * as $ from "@goscript/builtin/index.js" +import * as strings from "@goscript/strings/index.js" + +export async function main(): globalThis.Promise { + // CutLast splits around the last separator, like Cut does around the first. + let [before, after, found] = strings.CutLast("a/b/c", "/") + await $.println("CutLast:", before, after, found) + let __goscriptTuple0: any = strings.Cut("a/b/c", "/") + before = __goscriptTuple0[0] + after = __goscriptTuple0[1] + found = __goscriptTuple0[2] + await $.println("Cut:", before, after, found) + + // Without the separator, it returns s, "", false. + let __goscriptTuple1: any = strings.CutLast("abc", "/") + before = __goscriptTuple1[0] + after = __goscriptTuple1[1] + found = __goscriptTuple1[2] + await $.println("missing:", before, $.stringEqual(after, ""), found) + + // A multi-byte separator and non-ASCII text cut at the last match. + let __goscriptTuple2: any = strings.CutLast("h\xc3\xa9llo::w\xc3\xb6rld::end", "::") + before = __goscriptTuple2[0] + after = __goscriptTuple2[1] + found = __goscriptTuple2[2] + await $.println("multi:", before, after, found) +} + +if ($.isMainScript(import.meta)) { + await main() +} diff --git a/tests/tests/strings_cutlast/tsconfig.json b/tests/tests/strings_cutlast/tsconfig.json new file mode 100644 index 000000000..0023539c6 --- /dev/null +++ b/tests/tests/strings_cutlast/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "paths": { + "*": [ + "./*" + ], + "@go/github.com/s4wave/goscript/*": [ + "../../../*" + ], + "@goscript/*": [ + "../../../tests/tests/strings_cutlast/run/output/@goscript/*", + "../../../gs/*", + "../../../tests/deps/*" + ], + "@goscript/github.com/s4wave/goscript/tests/tests/strings_cutlast/*": [ + "./*" + ] + } + }, + "extends": "../../../tests/tsconfig.base.json", + "include": [ + "index.ts", + "strings_cutlast.gs.ts" + ] +}