From 26c377d90efaa74cb726760645ae0e580724bfb9 Mon Sep 17 00:00:00 2001 From: ncave <777696+ncave@users.noreply.github.com> Date: Wed, 6 Nov 2024 18:28:20 -0800 Subject: [PATCH] [Rust] Updated string comparisons --- src/Fable.Cli/CHANGELOG.md | 1 + src/Fable.Transforms/Rust/Replacements.fs | 102 ++++++++++++---------- src/fable-library-rust/src/String.rs | 94 +++++++++++++++++--- tests/Rust/tests/src/StringTests.fs | 22 +++++ 4 files changed, 165 insertions(+), 54 deletions(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index e50f58253..3aca27d5c 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* [Rust] Updated string comparisons (by @ncave) * [Rust] Fixed derived traits mapping (by @ncave) ## 4.23.0 - 2024-10-28 diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index 0a92fd624..158ae5453 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -1235,15 +1235,15 @@ let getEnumerator com r t i (expr: Expr) = makeInstanceCall r t i expr "GetEnumerator" [] let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - let isIgnoreCase args = - match args with - | [] -> false - | [ BoolConst ignoreCase ] -> ignoreCase - | [ BoolConst ignoreCase; _cultureInfo ] -> ignoreCase - | [ NumberConst(NumberValue.Int32 kind, NumberInfo.IsEnum _) ] -> kind = 1 || kind = 3 || kind = 5 - | [ _cultureInfo; NumberConst(NumberValue.Int32 options, NumberInfo.IsEnum _) ] -> - (options &&& 1 <> 0) || (options &&& 268435456 <> 0) - | _ -> false + // let isIgnoreCase args = + // match args with + // | [] -> false + // | [ BoolConst ignoreCase ] -> ignoreCase + // | [ BoolConst ignoreCase; _cultureInfo ] -> ignoreCase + // | [ NumberConst(NumberValue.Int32 kind, NumberInfo.IsEnum _) ] -> kind = 1 || kind = 3 || kind = 5 + // | [ _cultureInfo; NumberConst(NumberValue.Int32 options, NumberInfo.IsEnum _) ] -> + // (options &&& 1 <> 0) || (options &&& 268435456 <> 0) + // | _ -> false match i.CompiledName, thisArg, args with | ".ctor", _, _ -> @@ -1255,28 +1255,44 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | _ -> None | "get_Length", Some c, _ -> Helper.LibCall(com, "String", "length", t, c :: args, ?loc = r) |> Some | "get_Chars", Some c, _ -> Helper.LibCall(com, "String", "getCharAt", t, c :: args, ?loc = r) |> Some - | ("Compare" | "CompareOrdinal"), None, _ -> - if i.CompiledName = "Compare" then - $"String.Compare will be compiled as String.CompareOrdinal" - |> addWarning com ctx.InlinePath r - + | "CompareOrdinal", None, _ -> match args with - | ExprType String :: ExprType String :: restArgs -> - let args = (args |> List.take 2) @ [ makeBoolConst (isIgnoreCase restArgs) ] - + | [ ExprType String; ExprType String ] -> Helper.LibCall(com, "String", "compareOrdinal", t, args, ?loc = r) |> Some - | ExprType String :: ExprType(Number(Int32, _)) :: ExprType String :: ExprType(Number(Int32, _)) :: ExprType(Number(Int32, - _)) :: restArgs -> - let args = (args |> List.take 5) @ [ makeBoolConst (isIgnoreCase restArgs) ] - - Helper.LibCall(com, "String", "compareOrdinal2", t, args, ?loc = r) |> Some + | [ ExprType String + ExprType(Number(Int32, _)) + ExprType String + ExprType(Number(Int32, _)) + ExprType(Number(Int32, _)) ] -> Helper.LibCall(com, "String", "compareOrdinal2", t, args, ?loc = r) |> Some | _ -> None | "CompareTo", Some c, [ ExprTypeAs(String, arg) ] -> $"String.CompareTo will be compiled as String.CompareOrdinal" |> addWarning com ctx.InlinePath r - Helper.LibCall(com, "String", "compareOrdinal", t, [ c; arg; makeBoolConst false ], ?loc = r) - |> Some + Helper.LibCall(com, "String", "compareOrdinal", t, [ c; arg ], ?loc = r) |> Some + | "Compare", None, _ -> + $"String.Compare will be compiled as String.CompareOrdinal" + |> addWarning com ctx.InlinePath r + + match args with + | [ ExprType String; ExprType String ] -> + Helper.LibCall(com, "String", "compareOrdinal", t, args, ?loc = r) |> Some + | ExprType String :: ExprType String :: ExprType Boolean :: restArgs -> + Helper.LibCall(com, "String", "compareCase", t, args, ?loc = r) |> Some + | [ ExprType String; ExprType String; comparison ] -> + Helper.LibCall(com, "String", "compareWith", t, args, ?loc = r) |> Some + | [ ExprType String + ExprType(Number(Int32, _)) + ExprType String + ExprType(Number(Int32, _)) + ExprType(Number(Int32, _)) ] -> Helper.LibCall(com, "String", "compareOrdinal2", t, args, ?loc = r) |> Some + | ExprType String :: ExprType(Number(Int32, _)) :: ExprType String :: ExprType(Number(Int32, _)) :: ExprType(Number(Int32, + _)) :: ExprType Boolean :: restArgs -> + Helper.LibCall(com, "String", "compareCase2", t, args, ?loc = r) |> Some + | ExprType String :: ExprType(Number(Int32, _)) :: ExprType String :: ExprType(Number(Int32, _)) :: ExprType(Number(Int32, + _)) :: comparison :: restArgs -> + Helper.LibCall(com, "String", "compareWith2", t, args, ?loc = r) |> Some + | _ -> None | "Concat", None, _ -> match args with | [ ExprTypeAs(IEnumerable, arg) ] -> @@ -1292,32 +1308,28 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | "Contains", Some c, _ -> match args with | [ ExprType Char ] -> Helper.LibCall(com, "String", "containsChar", t, c :: args, ?loc = r) |> Some + | [ ExprType Char; _comparison ] -> + Helper.LibCall(com, "String", "containsChar2", t, c :: args, ?loc = r) |> Some | [ ExprType String ] -> Helper.LibCall(com, "String", "contains", t, c :: args, ?loc = r) |> Some + | [ ExprType String; _comparison ] -> Helper.LibCall(com, "String", "contains2", t, c :: args, ?loc = r) |> Some | _ -> None | "EndsWith", Some c, _ -> match args with | [ ExprType Char ] -> Helper.LibCall(com, "String", "endsWithChar", t, c :: args, ?loc = r) |> Some - | ExprType String :: restArgs -> - let args = (args |> List.take 1) @ [ makeBoolConst (isIgnoreCase restArgs) ] - - Helper.LibCall(com, "String", "endsWith", t, c :: args, ?loc = r) |> Some + | [ ExprType String ] -> Helper.LibCall(com, "String", "endsWith", t, c :: args, ?loc = r) |> Some + | [ ExprType String; _comparison ] -> Helper.LibCall(com, "String", "endsWith2", t, c :: args, ?loc = r) |> Some + | [ pattern; ignoreCase; _culture ] -> + Helper.LibCall(com, "String", "endsWith3", t, [ c; pattern; ignoreCase ], ?loc = r) + |> Some | _ -> None | "Equals", _, _ -> match thisArg, args with | Some x, [ ExprTypeAs(String, y) ] | None, [ ExprTypeAs(String, x); ExprTypeAs(String, y) ] -> - Helper.LibCall(com, "String", "equalsOrdinal", t, [ x; y; makeBoolConst false ], ?loc = r) - |> Some - | Some x, [ ExprTypeAs(String, y); NumberConst(NumberValue.Int32 kind, NumberInfo.IsEnum _) ] - | None, - [ ExprTypeAs(String, x); ExprTypeAs(String, y); NumberConst(NumberValue.Int32 kind, NumberInfo.IsEnum _) ] -> - if kind <> 4 && kind <> 5 then - $"String.Equals will be compiled with ordinal equality" - |> addWarning com ctx.InlinePath r - - let ignoreCase = kind = 1 || kind = 3 || kind = 5 - - Helper.LibCall(com, "String", "equalsOrdinal", t, [ x; y; makeBoolConst ignoreCase ], ?loc = r) + Helper.LibCall(com, "String", "equalsOrdinal", t, [ x; y ], ?loc = r) |> Some + | Some x, [ ExprTypeAs(String, y); comparison ] + | None, [ ExprTypeAs(String, x); ExprTypeAs(String, y); comparison ] -> + Helper.LibCall(com, "String", "equals2", t, [ x; y; comparison ], ?loc = r) |> Some | _ -> None | "Format", None, _ -> @@ -1487,10 +1499,12 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | "StartsWith", Some c, _ -> match args with | [ ExprType Char ] -> Helper.LibCall(com, "String", "startsWithChar", t, c :: args, ?loc = r) |> Some - | ExprType String :: restArgs -> - let args = (args |> List.take 1) @ [ makeBoolConst (isIgnoreCase restArgs) ] - - Helper.LibCall(com, "String", "startsWith", t, c :: args, ?loc = r) |> Some + | [ ExprType String ] -> Helper.LibCall(com, "String", "startsWith", t, c :: args, ?loc = r) |> Some + | [ ExprType String; _comparison ] -> + Helper.LibCall(com, "String", "startsWith2", t, c :: args, ?loc = r) |> Some + | [ pattern; ignoreCase; _culture ] -> + Helper.LibCall(com, "String", "startsWith3", t, [ c; pattern; ignoreCase ], ?loc = r) + |> Some | _ -> None | "Substring", Some c, _ -> match args with diff --git a/src/fable-library-rust/src/String.rs b/src/fable-library-rust/src/String.rs index 95f897332..f54fe906c 100644 --- a/src/fable-library-rust/src/String.rs +++ b/src/fable-library-rust/src/String.rs @@ -339,60 +339,134 @@ pub mod String_ { fromIter(a.iter().copied().skip(i as usize).take(count as usize)) } + // pub mod StringComparison { + // pub const CurrentCulture: i32 = 0; + // pub const CurrentCultureIgnoreCase: i32 = 1; + // pub const InvariantCulture: i32 = 2; + // pub const InvariantCultureIgnoreCase: i32 = 3; + // pub const Ordinal: i32 = 4; + // pub const OrdinalIgnoreCase: i32 = 5; + // } + + fn isIgnoreCase(comparison: i32) -> bool { + comparison == 1 || comparison == 3 || comparison == 5 + } + pub fn containsChar(s: string, c: char) -> bool { s.contains(c) } + pub fn containsChar2(s: string, c: char, comparison: i32) -> bool { + if isIgnoreCase(comparison) { + let mut buf = [0u8; 4]; + let c_str = c.encode_utf8(&mut buf); + s.to_uppercase().contains(&c_str.to_uppercase()) + } else { + s.contains(c) + } + } + pub fn contains(s: string, p: string) -> bool { s.contains(p.as_str()) } - pub fn equalsOrdinal(s1: string, s2: string, ignoreCase: bool) -> bool { - if ignoreCase { + pub fn contains2(s: string, p: string, comparison: i32) -> bool { + if isIgnoreCase(comparison) { + s.to_uppercase().contains(&p.to_uppercase()) + } else { + s.contains(p.as_str()) + } + } + + pub fn equalsOrdinal(s1: string, s2: string) -> bool { + s1.eq(&s2) + } + + pub fn equals2(s1: string, s2: string, comparison: i32) -> bool { + if isIgnoreCase(comparison) { s1.to_uppercase().eq(&s2.to_uppercase()) } else { s1.eq(&s2) } } - pub fn compareOrdinal(s1: string, s2: string, ignoreCase: bool) -> i32 { - if ignoreCase { + pub fn compareOrdinal(s1: string, s2: string) -> i32 { + compare(&s1, &s2) + } + + pub fn compareOrdinal2(s1: string, i1: i32, s2: string, i2: i32, count: i32) -> i32 { + let s1 = substring2(s1, i1, count); + let s2 = substring2(s2, i2, count); + compareOrdinal(s1, s2) + } + + pub fn compareWith(s1: string, s2: string, comparison: i32) -> i32 { + if isIgnoreCase(comparison) { compare(&s1.to_uppercase(), &s2.to_uppercase()) } else { compare(&s1, &s2) } } - pub fn compareOrdinal2(s1: string, i1: i32, s2: string, i2: i32, count: i32, ignoreCase: bool) -> i32 { + pub fn compareWith2(s1: string, i1: i32, s2: string, i2: i32, count: i32, comparison: i32) -> i32 { + let s1 = substring2(s1, i1, count); + let s2 = substring2(s2, i2, count); + compareWith(s1, s2, comparison) + } + + pub fn compareCase(s1: string, s2: string, ignoreCase: bool) -> i32 { + let comparison = if ignoreCase { 5 } else { 4 }; + compareWith(s1, s2, comparison) + } + + pub fn compareCase2(s1: string, i1: i32, s2: string, i2: i32, count: i32, ignoreCase: bool) -> i32 { let s1 = substring2(s1, i1, count); let s2 = substring2(s2, i2, count); - compareOrdinal(s1, s2, ignoreCase) + compareCase(s1, s2, ignoreCase) } pub fn startsWithChar(s: string, c: char) -> bool { s.starts_with(c) } - pub fn startsWith(s: string, p: string, ignoreCase: bool) -> bool { - if ignoreCase { + pub fn startsWith(s: string, p: string) -> bool { + s.starts_with(p.as_str()) + } + + pub fn startsWith2(s: string, p: string, comparison: i32) -> bool { + if isIgnoreCase(comparison) { s.to_uppercase().starts_with(&p.to_uppercase()) } else { s.starts_with(p.as_str()) } } + pub fn startsWith3(s: string, p: string, ignoreCase: bool) -> bool { + let comparison = if ignoreCase { 5 } else { 4 }; + startsWith2(s, p, comparison) + } + pub fn endsWithChar(s: string, c: char) -> bool { s.ends_with(c) } - pub fn endsWith(s: string, p: string, ignoreCase: bool) -> bool { - if ignoreCase { + pub fn endsWith(s: string, p: string) -> bool { + s.ends_with(p.as_str()) + } + + pub fn endsWith2(s: string, p: string, comparison: i32) -> bool { + if isIgnoreCase(comparison) { s.to_uppercase().ends_with(&p.to_uppercase()) } else { s.ends_with(p.as_str()) } } + pub fn endsWith3(s: string, p: string, ignoreCase: bool) -> bool { + let comparison = if ignoreCase { 5 } else { 4 }; + endsWith2(s, p, comparison) + } + pub fn isEmpty(s: string) -> bool { s.is_empty() } diff --git a/tests/Rust/tests/src/StringTests.fs b/tests/Rust/tests/src/StringTests.fs index b6b8f5129..23bc18fc6 100644 --- a/tests/Rust/tests/src/StringTests.fs +++ b/tests/Rust/tests/src/StringTests.fs @@ -813,6 +813,11 @@ let ``String.Compare substring works`` () = String.Compare("abc", 0, "bcd", 0, 3) |> equal -1 String.Compare("abc", 1, "bcd", 0, 2) |> equal 0 +[] +let ``String.Compare substring case-insensitive works`` () = + String.Compare("ABC", 0, "bcd", 0, 3, true) |> equal -1 + String.Compare("ABC", 1, "bcd", 0, 2, true) |> equal 0 + [] let ``String.Compare with comparison works`` () = // String.Compare("ABC", "abc", StringComparison.InvariantCulture) > 0 |> equal true @@ -846,6 +851,23 @@ let ``String.Contains works`` () = "ABC".Contains("B") |> equal true "ABC".Contains("Z") |> equal false +[] +let ``String.Contains with char works`` () = + "ABC".Contains('B') |> equal true + "ABC".Contains('Z') |> equal false + +[] +let ``String.Contains with comparison works`` () = + "ABC".Contains("b", StringComparison.OrdinalIgnoreCase) |> equal true + "Abc".Contains("B", StringComparison.OrdinalIgnoreCase) |> equal true + "Abc".Contains("B", StringComparison.Ordinal) |> equal false + +[] +let ``String.Contains with char comparison works`` () = + "ABC".Contains('b', StringComparison.OrdinalIgnoreCase) |> equal true + "Abc".Contains('B', StringComparison.OrdinalIgnoreCase) |> equal true + "Abc".Contains('B', StringComparison.Ordinal) |> equal false + [] let ``String.PadLeft works`` () = "3.14".PadLeft(10) |> equal " 3.14"