diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index c2f0b3a45..cb5477f66 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed * [Rust] Fixed try finally handler order of execution (by @ncave) +* [JS/TS/Python/Rust] Fixed String.StartsWith/EndsWith (#3934) (by @ncave) ## 4.22.0 - 2024-10-02 diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 475110fcb..71d8883b0 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -1313,7 +1313,6 @@ let implementedStringFunctions = [| "Compare" "CompareTo" - "EndsWith" "Format" "IndexOfAny" "Insert" @@ -1369,12 +1368,29 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt makeEqOp r left (makeIntConst 0) BinaryGreaterOrEqual |> Some | "StartsWith", Some c, [ _str ] -> - let left = Helper.InstanceCall(c, "find", Int32.Number, args) - - makeEqOp r left (makeIntConst 0) BinaryEqual |> Some + Helper.LibCall(com, "string", "startsWithExact", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) + |> Some | "StartsWith", Some c, [ _str; _comp ] -> Helper.LibCall(com, "string", "startsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) |> Some + | "StartsWith", Some c, [ value; ignoreCase; _culture ] -> + addWarning com ctx.InlinePath r "CultureInfo argument is ignored" + let args = [ value; ignoreCase ] + + Helper.LibCall(com, "string", "startsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) + |> Some + | "EndsWith", Some c, [ _str ] -> + Helper.LibCall(com, "string", "endsWithExact", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) + |> Some + | "EndsWith", Some c, [ _str; _comp ] -> + Helper.LibCall(com, "string", "endsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) + |> Some + | "EndsWith", Some c, [ value; ignoreCase; _culture ] -> + addWarning com ctx.InlinePath r "CultureInfo argument is ignored" + let args = [ value; ignoreCase ] + + Helper.LibCall(com, "string", "endsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) + |> Some | ReplaceName [ "ToUpper", "upper"; "ToUpperInvariant", "upper"; "ToLower", "lower"; "ToLowerInvariant", "lower" ] methName, Some c, args -> Helper.InstanceCall(c, methName, t, args, i.SignatureArgTypes, ?loc = r) |> Some diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index e909a8d18..d3f19c259 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -1509,12 +1509,24 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt makeEqOp r left (makeIntConst 0) BinaryGreaterOrEqual |> Some | "StartsWith", Some c, [ _str ] -> Helper.InstanceCall(c, "startsWith", Boolean, args) |> Some | "StartsWith", Some c, [ _str; _comp ] -> + Helper.LibCall(com, "String", "startsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) + |> Some + | "StartsWith", Some c, [ value; ignoreCase; _culture ] -> + addWarning com ctx.InlinePath r "CultureInfo argument is ignored" + let args = [ value; ignoreCase ] + Helper.LibCall(com, "String", "startsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) |> Some | "EndsWith", Some c, [ _str ] -> Helper.InstanceCall(c, "endsWith", Boolean, args) |> Some | "EndsWith", Some c, [ _str; _comp ] -> Helper.LibCall(com, "String", "endsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) |> Some + | "EndsWith", Some c, [ value; ignoreCase; _culture ] -> + addWarning com ctx.InlinePath r "CultureInfo argument is ignored" + let args = [ value; ignoreCase ] + + Helper.LibCall(com, "String", "endsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) + |> Some | ReplaceName [ "ToUpper", "toLocaleUpperCase" "ToUpperInvariant", "toUpperCase" diff --git a/src/fable-library-py/fable_library/string_.py b/src/fable-library-py/fable_library/string_.py index 4b4729156..d05d7df0b 100644 --- a/src/fable-library-py/fable_library/string_.py +++ b/src/fable-library-py/fable_library/string_.py @@ -478,8 +478,7 @@ def compare(string1: str, string2: str, /) -> int: @overload -def compare(string1: str, string2: str, ignore_case: bool, culture: StringComparison, /) -> int: - ... +def compare(string1: str, string2: str, ignore_case: bool, culture: StringComparison, /) -> int: ... def compare(*args: Any) -> int: @@ -527,15 +526,25 @@ def compare_to(this: str, other: str) -> int: return cmp(this, other, StringComparison.CurrentCulture) -def ends_with(string: str, search: str): - idx = string.rfind(search) - return idx >= 0 and idx == len(string) - len(search) +def ends_with_exact(string: str, pattern: str): + idx = string.rfind(pattern) + return idx >= 0 and idx == len(string) - len(pattern) -def starts_with(string: str, pattern: str, ic: int): +def ends_with(string: str, pattern: str, ic: bool | StringComparison): if len(string) >= len(pattern): - return cmp(string[0 : len(pattern)], pattern, True if ic else False) == 0 + return cmp(string[len(string) - len(pattern) : len(string)], pattern, ic) == 0 + return False + +def starts_with_exact(string: str, pattern: str): + idx = string.find(pattern) + return idx == 0 + + +def starts_with(string: str, pattern: str, ic: bool | StringComparison): + if len(string) >= len(pattern): + return cmp(string[0 : len(pattern)], pattern, ic) == 0 return False diff --git a/src/fable-library-ts/String.ts b/src/fable-library-ts/String.ts index 6b58e844b..6379ce78a 100644 --- a/src/fable-library-ts/String.ts +++ b/src/fable-library-ts/String.ts @@ -63,7 +63,7 @@ export function compareTo(x: string, y: string) { return cmp(x, y, StringComparison.CurrentCulture); } -export function startsWith(str: string, pattern: string, ic: number) { +export function startsWith(str: string, pattern: string, ic: boolean | StringComparison) { if (ic === StringComparison.Ordinal) { // to avoid substring allocation return str.startsWith(pattern); } @@ -73,7 +73,7 @@ export function startsWith(str: string, pattern: string, ic: number) { return false; } -export function endsWith(str: string, pattern: string, ic: number) { +export function endsWith(str: string, pattern: string, ic: boolean | StringComparison) { if (ic === StringComparison.Ordinal) { // to avoid substring allocation return str.endsWith(pattern); } diff --git a/tests/Dart/src/StringTests.fs b/tests/Dart/src/StringTests.fs index c59d91fd6..67cc6b62a 100644 --- a/tests/Dart/src/StringTests.fs +++ b/tests/Dart/src/StringTests.fs @@ -800,24 +800,49 @@ let tests() = // failing //"abcdbcebc".IndexOfAny([|'c';'b'|]) |> equal 1 + // testCase "String.StartsWith char works" <| fun () -> + // "abcd".StartsWith('a') |> equal true + // "abcd".StartsWith('d') |> equal false + + // testCase "String.EndsWith char works" <| fun () -> + // "abcd".EndsWith('a') |> equal false + // "abcd".EndsWith('d') |> equal true + testCase "String.StartsWith works" <| fun () -> - let args = [("ab", true); ("cd", false); ("abcdx", false)] + let args = [("ab", true); ("bc", false); ("cd", false); ("abcdx", false); ("abcd", true)] for arg in args do "abcd".StartsWith(fst arg) |> equal (snd arg) - // // TODO: StartsWith with StringComparison - // testCase "String.StartsWith with StringComparison works" <| fun () -> - // let args = [("ab", true); ("cd", false); ("abcdx", false)] + // testCase "String.StartsWith with OrdinalIgnoreCase works" <| fun () -> + // let args = [("ab", true); ("AB", true); ("BC", false); ("cd", false); ("abcdx", false); ("abcd", true)] // for arg in args do // "ABCD".StartsWith(fst arg, StringComparison.OrdinalIgnoreCase) // |> equal (snd arg) + // testCase "String.StartsWith with ignoreCase boolean works" <| fun () -> + // let args = [("ab", true); ("AB", true); ("BC", false); ("cd", false); ("abcdx", false); ("abcd", true)] + // for arg in args do + // "ABCD".StartsWith(fst arg, true, CultureInfo.InvariantCulture) + // |> equal (snd arg) + testCase "String.EndsWith works" <| fun () -> - let args = [("ab", false); ("cd", true); ("abcdx", false)] + let args = [("ab", false); ("cd", true); ("bc", false); ("abcdx", false); ("abcd", true)] for arg in args do - "abcd".EndsWith(fst arg) - |> equal (snd arg) + "abcd".EndsWith(fst arg) + |> equal (snd arg) + + // testCase "String.EndsWith with OrdinalIgnoreCase works" <| fun () -> + // let args = [("ab", false); ("CD", true); ("cd", true); ("bc", false); ("xabcd", false); ("abcd", true)] + // for arg in args do + // "ABCD".EndsWith(fst arg, StringComparison.OrdinalIgnoreCase) + // |> equal (snd arg) + + // testCase "String.EndsWith with ignoreCase boolean works" <| fun () -> + // let args = [("ab", false); ("CD", true); ("cd", true); ("bc", false); ("xabcd", false); ("abcd", true)] + // for arg in args do + // "ABCD".EndsWith(fst arg, true, CultureInfo.InvariantCulture) + // |> equal (snd arg) testCase "String.Trim works" <| fun () -> " abc ".Trim() diff --git a/tests/Js/Main/StringTests.fs b/tests/Js/Main/StringTests.fs index 8d420af31..6c3d568a9 100644 --- a/tests/Js/Main/StringTests.fs +++ b/tests/Js/Main/StringTests.fs @@ -814,29 +814,49 @@ let tests = testList "Strings" [ "abcdbcebc".IndexOfAny([|'f';'e'|], 2, 4) |> equal -1 "abcdbcebc".IndexOfAny([|'c';'b'|]) |> equal 1 + // testCase "String.StartsWith char works" <| fun () -> + // "abcd".StartsWith('a') |> equal true + // "abcd".StartsWith('d') |> equal false + + // testCase "String.EndsWith char works" <| fun () -> + // "abcd".EndsWith('a') |> equal false + // "abcd".EndsWith('d') |> equal true + testCase "String.StartsWith works" <| fun () -> let args = [("ab", true); ("bc", false); ("cd", false); ("abcdx", false); ("abcd", true)] for arg in args do - "abcd".StartsWith(fst arg) - |> equal (snd arg) + "abcd".StartsWith(fst arg) + |> equal (snd arg) testCase "String.StartsWith with OrdinalIgnoreCase works" <| fun () -> let args = [("ab", true); ("AB", true); ("BC", false); ("cd", false); ("abcdx", false); ("abcd", true)] for arg in args do - "ABCD".StartsWith(fst arg, StringComparison.OrdinalIgnoreCase) - |> equal (snd arg) + "ABCD".StartsWith(fst arg, StringComparison.OrdinalIgnoreCase) + |> equal (snd arg) + + testCase "String.StartsWith with ignoreCase boolean works" <| fun () -> + let args = [("ab", true); ("AB", true); ("BC", false); ("cd", false); ("abcdx", false); ("abcd", true)] + for arg in args do + "ABCD".StartsWith(fst arg, true, CultureInfo.InvariantCulture) + |> equal (snd arg) testCase "String.EndsWith works" <| fun () -> let args = [("ab", false); ("cd", true); ("bc", false); ("abcdx", false); ("abcd", true)] for arg in args do - "abcd".EndsWith(fst arg) - |> equal (snd arg) + "abcd".EndsWith(fst arg) + |> equal (snd arg) testCase "String.EndsWith with OrdinalIgnoreCase works" <| fun () -> let args = [("ab", false); ("CD", true); ("cd", true); ("bc", false); ("xabcd", false); ("abcd", true)] for arg in args do - "ABCD".EndsWith(fst arg, StringComparison.OrdinalIgnoreCase) - |> equal (snd arg) + "ABCD".EndsWith(fst arg, StringComparison.OrdinalIgnoreCase) + |> equal (snd arg) + + testCase "String.EndsWith with ignoreCase boolean works" <| fun () -> + let args = [("ab", false); ("CD", true); ("cd", true); ("bc", false); ("xabcd", false); ("abcd", true)] + for arg in args do + "ABCD".EndsWith(fst arg, true, CultureInfo.InvariantCulture) + |> equal (snd arg) testCase "String.Trim works" <| fun () -> " abc ".Trim() diff --git a/tests/Python/TestString.fs b/tests/Python/TestString.fs index d3d92b119..e2f5e8304 100644 --- a/tests/Python/TestString.fs +++ b/tests/Python/TestString.fs @@ -553,26 +553,57 @@ let ``test String.IndexOfAny works`` () = "abcdbcebc".IndexOfAny([|'f';'e'|], 2, 4) |> equal -1 "abcdbcebc".IndexOfAny([|'c';'b'|]) |> equal 1 +// [] +// let ``test String.StartsWith char works`` () = +// "abcd".StartsWith('a') |> equal true +// "abcd".StartsWith('d') |> equal false + +// [] +// let ``test String.EndsWith char works`` () = +// "abcd".EndsWith('a') |> equal false +// "abcd".EndsWith('d') |> equal true + [] let ``test String.StartsWith works`` () = - let args = [("ab", true); ("cd", false); ("abcdx", false)] + let args = [("ab", true); ("bc", false); ("cd", false); ("abcdx", false); ("abcd", true)] for arg in args do - "abcd".StartsWith(fst arg) - |> equal (snd arg) + "abcd".StartsWith(fst arg) + |> equal (snd arg) [] -let ``test String.StartsWith with StringComparison works`` () = - let args = [("ab", true); ("cd", false); ("abcdx", false)] +let ``test String.StartsWith with OrdinalIgnoreCase works`` () = + let args = [("ab", true); ("AB", true); ("BC", false); ("cd", false); ("abcdx", false); ("abcd", true)] for arg in args do - "ABCD".StartsWith(fst arg, StringComparison.OrdinalIgnoreCase) - |> equal (snd arg) + "ABCD".StartsWith(fst arg, StringComparison.OrdinalIgnoreCase) + |> equal (snd arg) + +[] +let ``test String.StartsWith with ignoreCase boolean works`` () = + let args = [("ab", true); ("AB", true); ("BC", false); ("cd", false); ("abcdx", false); ("abcd", true)] + for arg in args do + "ABCD".StartsWith(fst arg, true, CultureInfo.InvariantCulture) + |> equal (snd arg) [] let ``test String.EndsWith works`` () = - let args = [("ab", false); ("cd", true); ("abcdx", false)] + let args = [("ab", false); ("cd", true); ("bc", false); ("abcdx", false); ("abcd", true)] for arg in args do - "abcd".EndsWith(fst arg) - |> equal (snd arg) + "abcd".EndsWith(fst arg) + |> equal (snd arg) + +[] +let ``test String.EndsWith with OrdinalIgnoreCase works`` () = + let args = [("ab", false); ("CD", true); ("cd", true); ("bc", false); ("xabcd", false); ("abcd", true)] + for arg in args do + "ABCD".EndsWith(fst arg, StringComparison.OrdinalIgnoreCase) + |> equal (snd arg) + +[] +let ``test String.EndsWith with ignoreCase boolean works`` () = + let args = [("ab", false); ("CD", true); ("cd", true); ("bc", false); ("xabcd", false); ("abcd", true)] + for arg in args do + "ABCD".EndsWith(fst arg, true, CultureInfo.InvariantCulture) + |> equal (snd arg) [] let ``test String.Trim works`` () = diff --git a/tests/Rust/tests/src/StringTests.fs b/tests/Rust/tests/src/StringTests.fs index fa3feb33a..b6b8f5129 100644 --- a/tests/Rust/tests/src/StringTests.fs +++ b/tests/Rust/tests/src/StringTests.fs @@ -1059,42 +1059,58 @@ let ``String.LastIndexOfAny works`` () = "abcdbcebc".LastIndexOfAny([|'f';'e'|], 6) |> equal 6 "abcdbcebc".LastIndexOfAny([|'f';'e'|], 7, 1) |> equal -1 -[] -let ``String.StartsWith works`` () = - "abcd".StartsWith("ab") |> equal true - "abcd".StartsWith("cd") |> equal false - "abcd".StartsWith("abcdx") |> equal false - [] let ``String.StartsWith char works`` () = "abcd".StartsWith('a') |> equal true "abcd".StartsWith('d') |> equal false [] -let ``String.StartsWith with StringComparison works`` () = - let args = [("ab", true); ("cd", false); ("abcdx", false)] +let ``String.EndsWith char works`` () = + "abcd".EndsWith('a') |> equal false + "abcd".EndsWith('d') |> equal true + +[] +let ``String.StartsWith works`` () = + let args = [("ab", true); ("bc", false); ("cd", false); ("abcdx", false); ("abcd", true)] + for arg in args do + "abcd".StartsWith(fst arg) + |> equal (snd arg) + +[] +let ``String.StartsWith with OrdinalIgnoreCase works`` () = + let args = [("ab", true); ("AB", true); ("BC", false); ("cd", false); ("abcdx", false); ("abcd", true)] for arg in args do "ABCD".StartsWith(fst arg, StringComparison.OrdinalIgnoreCase) |> equal (snd arg) [] -let ``String.EndsWith works`` () = - "abcd".EndsWith("ab") |> equal false - "abcd".EndsWith("cd") |> equal true - "abcd".EndsWith("abcdx") |> equal false +let ``String.StartsWith with ignoreCase boolean works`` () = + let args = [("ab", true); ("AB", true); ("BC", false); ("cd", false); ("abcdx", false); ("abcd", true)] + for arg in args do + "ABCD".StartsWith(fst arg, true, CultureInfo.InvariantCulture) + |> equal (snd arg) [] -let ``String.EndsWith char works`` () = - "abcd".EndsWith('a') |> equal false - "abcd".EndsWith('d') |> equal true +let ``String.EndsWith works`` () = + let args = [("ab", false); ("cd", true); ("bc", false); ("abcdx", false); ("abcd", true)] + for arg in args do + "abcd".EndsWith(fst arg) + |> equal (snd arg) [] -let ``String.EndsWith with StringComparison works`` () = - let args = [("ab", false); ("cd", true); ("abcdx", false)] +let ``String.EndsWith with OrdinalIgnoreCase works`` () = + let args = [("ab", false); ("CD", true); ("cd", true); ("bc", false); ("xabcd", false); ("abcd", true)] for arg in args do "ABCD".EndsWith(fst arg, StringComparison.OrdinalIgnoreCase) |> equal (snd arg) +[] +let ``String.EndsWith with ignoreCase boolean works`` () = + let args = [("ab", false); ("CD", true); ("cd", true); ("bc", false); ("xabcd", false); ("abcd", true)] + for arg in args do + "ABCD".EndsWith(fst arg, true, CultureInfo.InvariantCulture) + |> equal (snd arg) + [] let ``String.Trim works`` () = " abc ".Trim()