Skip to content

Commit

Permalink
Fixed String.StartsWith/EndsWith (fable-compiler#3934) (fable-compile…
Browse files Browse the repository at this point in the history
  • Loading branch information
ncave authored Oct 22, 2024
1 parent 31019c8 commit 619ab07
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 55 deletions.
1 change: 1 addition & 0 deletions src/Fable.Cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 20 additions & 4 deletions src/Fable.Transforms/Python/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1313,7 +1313,6 @@ let implementedStringFunctions =
[|
"Compare"
"CompareTo"
"EndsWith"
"Format"
"IndexOfAny"
"Insert"
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions src/Fable.Transforms/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
23 changes: 16 additions & 7 deletions src/fable-library-py/fable_library/string_.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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


Expand Down
4 changes: 2 additions & 2 deletions src/fable-library-ts/String.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down
39 changes: 32 additions & 7 deletions tests/Dart/src/StringTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
36 changes: 28 additions & 8 deletions tests/Js/Main/StringTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
51 changes: 41 additions & 10 deletions tests/Python/TestString.fs
Original file line number Diff line number Diff line change
Expand Up @@ -553,26 +553,57 @@ let ``test String.IndexOfAny works`` () =
"abcdbcebc".IndexOfAny([|'f';'e'|], 2, 4) |> equal -1
"abcdbcebc".IndexOfAny([|'c';'b'|]) |> equal 1

// [<Fact>]
// let ``test String.StartsWith char works`` () =
// "abcd".StartsWith('a') |> equal true
// "abcd".StartsWith('d') |> equal false

// [<Fact>]
// let ``test String.EndsWith char works`` () =
// "abcd".EndsWith('a') |> equal false
// "abcd".EndsWith('d') |> equal true

[<Fact>]
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)

[<Fact>]
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)

[<Fact>]
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)

[<Fact>]
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)

[<Fact>]
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)

[<Fact>]
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)

[<Fact>]
let ``test String.Trim works`` () =
Expand Down
50 changes: 33 additions & 17 deletions tests/Rust/tests/src/StringTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1059,42 +1059,58 @@ let ``String.LastIndexOfAny works`` () =
"abcdbcebc".LastIndexOfAny([|'f';'e'|], 6) |> equal 6
"abcdbcebc".LastIndexOfAny([|'f';'e'|], 7, 1) |> equal -1

[<Fact>]
let ``String.StartsWith works`` () =
"abcd".StartsWith("ab") |> equal true
"abcd".StartsWith("cd") |> equal false
"abcd".StartsWith("abcdx") |> equal false

[<Fact>]
let ``String.StartsWith char works`` () =
"abcd".StartsWith('a') |> equal true
"abcd".StartsWith('d') |> equal false

[<Fact>]
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

[<Fact>]
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)

[<Fact>]
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)

[<Fact>]
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)

[<Fact>]
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)

[<Fact>]
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)

[<Fact>]
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)

[<Fact>]
let ``String.Trim works`` () =
" abc ".Trim()
Expand Down

0 comments on commit 619ab07

Please sign in to comment.