diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 4b9e7a1200..c424556c71 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -1557,6 +1557,11 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | [ ExprType String ] -> Helper.InstanceCall(c, "rfind", t, args, i.SignatureArgTypes, ?loc = r) |> Some | [ ExprType Char as str; ExprType(Number(Int32, NumberInfo.Empty)) as start ] | [ ExprType String as str; ExprType(Number(Int32, NumberInfo.Empty)) as start ] -> + // .NET's LastIndexOf(value, startIndex) searches the inclusive window [0, startIndex], + // so the exclusive end passed to Python's rfind must be startIndex + 1. + let endIdx = + makeBinOp r (Number(Int32, NumberInfo.Empty)) start (makeIntConst 1) BinaryPlus + Helper.InstanceCall( c, "rfind", @@ -1564,7 +1569,7 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt [ str Value(NumberConstant(NumberValue.Int32 0, NumberInfo.Empty), None) - start + endIdx ], i.SignatureArgTypes, ?loc = r diff --git a/src/fable-library-beam/fable_string.erl b/src/fable-library-beam/fable_string.erl index 870ff0da88..fc1ebc9dbf 100644 --- a/src/fable-library-beam/fable_string.erl +++ b/src/fable-library-beam/fable_string.erl @@ -135,9 +135,13 @@ remove(Str, StartIdx, Count) -> binary:part(Str, StartIdx + Count, byte_size(Str) - StartIdx - Count) ]). +starts_with(_Str, <<>>) -> + true; starts_with(Str, Prefix) -> binary:match(Str, Prefix) =:= {0, byte_size(Prefix)}. +ends_with(_Str, <<>>) -> + true; ends_with(Str, Suffix) -> SuffixLen = byte_size(Suffix), StrLen = byte_size(Str), @@ -262,6 +266,8 @@ filter(Fn, Str) -> index_of(Str, Sub) -> index_of(Str, Sub, 0). +index_of(_Str, <<>>, StartIdx) -> + StartIdx; index_of(Str, Sub, StartIdx) -> SearchStr = binary:part(Str, StartIdx, byte_size(Str) - StartIdx), case binary:match(SearchStr, Sub) of @@ -293,6 +299,8 @@ index_of_any_loop([C | Rest], CharSet, Idx, StartIdx) -> last_index_of(Str, Sub) -> last_index_of(Str, Sub, byte_size(Str) - 1). +last_index_of(Str, <<>>, MaxIdx) -> + erlang:min(MaxIdx + 1, byte_size(Str)); last_index_of(Str, Sub, MaxIdx) -> SubLen = byte_size(Sub), SearchLen = erlang:min(MaxIdx + SubLen, byte_size(Str)), @@ -302,6 +310,8 @@ last_index_of(Str, Sub, MaxIdx) -> Matches -> element(1, lists:last(Matches)) end. +contains(_Str, <<>>) -> + true; contains(Str, Sub) -> binary:match(Str, Sub) =/= nomatch. diff --git a/src/fable-library-ts/String.ts b/src/fable-library-ts/String.ts index 9973248754..6fc05aec75 100644 --- a/src/fable-library-ts/String.ts +++ b/src/fable-library-ts/String.ts @@ -81,7 +81,7 @@ export function endsWith(str: string, pattern: string, ic: boolean | StringCompa return str.endsWith(pattern); } if (str.length >= pattern.length) { - return cmp(str.slice(-pattern.length), pattern, ic) === 0; + return cmp(str.slice(str.length - pattern.length), pattern, ic) === 0; } return false; } diff --git a/tests/Beam/StringTests.fs b/tests/Beam/StringTests.fs index 206e0e9a30..b92abc6f43 100644 --- a/tests/Beam/StringTests.fs +++ b/tests/Beam/StringTests.fs @@ -172,6 +172,16 @@ let ``test String.EndsWith with OrdinalIgnoreCase works`` () = "ABCD".EndsWith("xabcd", StringComparison.OrdinalIgnoreCase) |> equal false "ABCD".EndsWith("abcd", StringComparison.OrdinalIgnoreCase) |> equal true +[] +let ``test String.StartsWith and String.EndsWith with empty pattern works`` () = + // .NET: every string starts/ends with "" + "abcd".StartsWith("") |> equal true + "abcd".EndsWith("") |> equal true + "".StartsWith("") |> equal true + "".EndsWith("") |> equal true + "abcd".StartsWith("", StringComparison.OrdinalIgnoreCase) |> equal true + "abcd".EndsWith("", StringComparison.OrdinalIgnoreCase) |> equal true + // --- Substring --- [] @@ -197,6 +207,12 @@ let ``test String.Contains works`` () = "ABC".Contains("B") |> equal true "ABC".Contains("Z") |> equal false +[] +let ``test String.Contains with empty pattern works`` () = + // .NET: every string contains "" + "ABC".Contains("") |> equal true + "".Contains("") |> equal true + // --- IndexOf --- [] @@ -207,6 +223,12 @@ let ``test String.IndexOf works`` () = let ``test String.IndexOf works with offset`` () = "abcdbc".IndexOf("bc", 3) |> equal 4 +[] +let ``test String.IndexOf with empty pattern works`` () = + "abcdbc".IndexOf("") |> equal 0 + "abcdbc".IndexOf("", 3) |> equal 3 + "abcdbc".IndexOf("", 6) |> equal 6 + [] let ``test String.IndexOf char works`` () = "abcd".IndexOf('b') * 100 + "abcd".IndexOf('e') |> equal 99 @@ -253,6 +275,12 @@ let ``test String.LastIndexOf with index and StringComparison`` () = "abcdbcebc".LastIndexOf("b", 3, StringComparison.Ordinal) |> equal 1 +[] +let ``test String.LastIndexOf with empty pattern works`` () = + "abcdbc".LastIndexOf("") |> equal 6 + "abcdbc".LastIndexOf("", 3) |> equal 4 + "abcdbc".LastIndexOf("", 0) |> equal 1 + // --- Access char by index --- [] diff --git a/tests/Js/Main/StringTests.fs b/tests/Js/Main/StringTests.fs index 591443b791..30800c08c9 100644 --- a/tests/Js/Main/StringTests.fs +++ b/tests/Js/Main/StringTests.fs @@ -1041,6 +1041,27 @@ let tests = testList "Strings" [ "ABCD".EndsWith(fst arg, true, CultureInfo.InvariantCulture) |> equal (snd arg) + testCase "String.EndsWith with empty pattern and StringComparison works" <| fun () -> + // .NET: every string ends with "" for all StringComparison values + "hello".EndsWith("", StringComparison.CurrentCulture) |> equal true + "hello".EndsWith("", StringComparison.InvariantCulture) |> equal true + "hello".EndsWith("", StringComparison.Ordinal) |> equal true + "hello".EndsWith("", StringComparison.OrdinalIgnoreCase) |> equal true + "hello".EndsWith("", StringComparison.InvariantCultureIgnoreCase) |> equal true + "".EndsWith("", StringComparison.CurrentCulture) |> equal true + "hello".StartsWith("", StringComparison.CurrentCulture) |> equal true + "hello".StartsWith("", StringComparison.Ordinal) |> equal true + "hello".StartsWith("", StringComparison.OrdinalIgnoreCase) |> equal true + + testCase "String.EndsWith with non-empty pattern and StringComparison works" <| fun () -> + "hello".EndsWith("LO", StringComparison.OrdinalIgnoreCase) |> equal true + "hello".EndsWith("LO", StringComparison.InvariantCultureIgnoreCase) |> equal true + "hello".EndsWith("lo", StringComparison.Ordinal) |> equal true + "hello".EndsWith("LO", StringComparison.Ordinal) |> equal false + "hello".EndsWith("he", StringComparison.Ordinal) |> equal false + "hello".EndsWith("hello", StringComparison.CurrentCulture) |> equal true + "hello".EndsWith("xhello", StringComparison.Ordinal) |> equal false + testCase "String.Trim works" <| fun () -> " abc ".Trim() |> equal "abc" diff --git a/tests/Python/TestString.fs b/tests/Python/TestString.fs index a8e09b0abc..bea0208a10 100644 --- a/tests/Python/TestString.fs +++ b/tests/Python/TestString.fs @@ -518,6 +518,12 @@ let ``test String.Contains works`` () = "ABC".Contains("B") |> equal true "ABC".Contains("Z") |> equal false +[] +let ``test String.Contains with empty pattern works`` () = + // .NET: every string contains "" + "ABC".Contains("") |> equal true + "".Contains("") |> equal true + [] let ``test String.Contains with StringComparison works`` () = "ABC".Contains("b", StringComparison.Ordinal) |> equal false @@ -594,6 +600,12 @@ let ``test String.IndexOf works with offset`` () = "abcdbc".IndexOf("bc", 3) |> equal 4 +[] +let ``test String.IndexOf with empty pattern works`` () = + "abcdbc".IndexOf("") |> equal 0 + "abcdbc".IndexOf("", 3) |> equal 3 + "abcdbc".IndexOf("", 6) |> equal 6 + [] let ``test String.LastIndexOf works`` () = "abcdbc".LastIndexOf("bc") * 100 + "abcd".LastIndexOf("bd") @@ -604,6 +616,12 @@ let ``test String.LastIndexOf works with offset`` () = "abcdbcebc".LastIndexOf("bc", 3) |> equal 1 +[] +let ``test String.LastIndexOf with empty pattern works`` () = + "abcdbc".LastIndexOf("") |> equal 6 + "abcdbc".LastIndexOf("", 3) |> equal 4 + "abcdbc".LastIndexOf("", 0) |> equal 1 + [] let ``test String.IndexOf with StringComparison works`` () = "abcdbc".IndexOf("b", StringComparison.Ordinal) @@ -652,42 +670,42 @@ let ``test String.EndsWith char works`` () = [] let ``test String.StartsWith works`` () = - let args = [("ab", true); ("bc", false); ("cd", false); ("abcdx", false); ("abcd", true)] + let args = [("ab", true); ("bc", false); ("cd", false); ("abcdx", false); ("abcd", true); ("", true)] for arg in args do "abcd".StartsWith(fst arg) |> equal (snd arg) [] let ``test String.StartsWith with OrdinalIgnoreCase works`` () = - let args = [("ab", true); ("AB", true); ("BC", false); ("cd", false); ("abcdx", false); ("abcd", true)] + let args = [("ab", true); ("AB", true); ("BC", false); ("cd", false); ("abcdx", false); ("abcd", true); ("", true)] for arg in args do "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)] + let args = [("ab", true); ("AB", true); ("BC", false); ("cd", false); ("abcdx", false); ("abcd", true); ("", 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); ("bc", false); ("abcdx", false); ("abcd", true)] + let args = [("ab", false); ("cd", true); ("bc", false); ("abcdx", false); ("abcd", true); ("", true)] for arg in args do "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)] + let args = [("ab", false); ("CD", true); ("cd", true); ("bc", false); ("xabcd", false); ("abcd", true); ("", 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)] + let args = [("ab", false); ("CD", true); ("cd", true); ("bc", false); ("xabcd", false); ("abcd", true); ("", true)] for arg in args do "ABCD".EndsWith(fst arg, true, CultureInfo.InvariantCulture) |> equal (snd arg) diff --git a/tests/Rust/tests/src/StringTests.fs b/tests/Rust/tests/src/StringTests.fs index 4902fa7213..2e57732910 100644 --- a/tests/Rust/tests/src/StringTests.fs +++ b/tests/Rust/tests/src/StringTests.fs @@ -880,6 +880,12 @@ let ``String.Contains works`` () = "ABC".Contains("B") |> equal true "ABC".Contains("Z") |> equal false +[] +let ``String.Contains with empty pattern works`` () = + // .NET: every string contains "" + "ABC".Contains("") |> equal true + "".Contains("") |> equal true + [] let ``String.Contains with char works`` () = "ABC".Contains('B') |> equal true @@ -1081,6 +1087,12 @@ let ``String.IndexOf works with offset`` () = "abcdbc".IndexOf("bc", 3) |> equal 4 +[] +let ``String.IndexOf with empty pattern works`` () = + "abcdbc".IndexOf("") |> equal 0 + "abcdbc".IndexOf("", 3) |> equal 3 + "abcdbc".IndexOf("", 6) |> equal 6 + [] let ``String.LastIndexOf works`` () = "abcdbc".LastIndexOf("bc") * 100 + "abcd".LastIndexOf("bd") @@ -1091,6 +1103,12 @@ let ``String.LastIndexOf works with offset`` () = "abcdbcebc".LastIndexOf("bc", 3) |> equal 1 +[] +let ``String.LastIndexOf with empty pattern works`` () = + "abcdbc".LastIndexOf("") |> equal 6 + "abcdbc".LastIndexOf("", 3) |> equal 4 + "abcdbc".LastIndexOf("", 0) |> equal 1 + [] let ``String.IndexOfAny works`` () = "abcdbcebc".IndexOfAny([|'b'|]) |> equal 1 @@ -1122,42 +1140,42 @@ let ``String.EndsWith char works`` () = [] let ``String.StartsWith works`` () = - let args = [("ab", true); ("bc", false); ("cd", false); ("abcdx", false); ("abcd", true)] + let args = [("ab", true); ("bc", false); ("cd", false); ("abcdx", false); ("abcd", true); ("", 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)] + let args = [("ab", true); ("AB", true); ("BC", false); ("cd", false); ("abcdx", false); ("abcd", true); ("", true)] for arg in args do "ABCD".StartsWith(fst arg, StringComparison.OrdinalIgnoreCase) |> equal (snd arg) [] let ``String.StartsWith with ignoreCase boolean works`` () = - let args = [("ab", true); ("AB", true); ("BC", false); ("cd", false); ("abcdx", false); ("abcd", true)] + let args = [("ab", true); ("AB", true); ("BC", false); ("cd", false); ("abcdx", false); ("abcd", true); ("", true)] for arg in args do "ABCD".StartsWith(fst arg, true, CultureInfo.InvariantCulture) |> equal (snd arg) [] let ``String.EndsWith works`` () = - let args = [("ab", false); ("cd", true); ("bc", false); ("abcdx", false); ("abcd", true)] + let args = [("ab", false); ("cd", true); ("bc", false); ("abcdx", false); ("abcd", true); ("", true)] for arg in args do "abcd".EndsWith(fst arg) |> equal (snd arg) [] let ``String.EndsWith with OrdinalIgnoreCase works`` () = - let args = [("ab", false); ("CD", true); ("cd", true); ("bc", false); ("xabcd", false); ("abcd", true)] + let args = [("ab", false); ("CD", true); ("cd", true); ("bc", false); ("xabcd", false); ("abcd", true); ("", 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)] + let args = [("ab", false); ("CD", true); ("cd", true); ("bc", false); ("xabcd", false); ("abcd", true); ("", true)] for arg in args do "ABCD".EndsWith(fst arg, true, CultureInfo.InvariantCulture) |> equal (snd arg)