Skip to content

Commit

Permalink
[Rust] Updated string comparisons (#3950)
Browse files Browse the repository at this point in the history
  • Loading branch information
ncave authored Nov 8, 2024
1 parent f0bcb6c commit 4612e59
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 54 deletions.
1 change: 1 addition & 0 deletions src/Fable.Cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
* [JS/TS] Added missing ICollection helpers (#3914) (by @ncave)

Expand Down
102 changes: 58 additions & 44 deletions src/Fable.Transforms/Rust/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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", _, _ ->
Expand All @@ -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) ] ->
Expand All @@ -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, _ ->
Expand Down Expand Up @@ -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
Expand Down
94 changes: 84 additions & 10 deletions src/fable-library-rust/src/String.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
22 changes: 22 additions & 0 deletions tests/Rust/tests/src/StringTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

[<Fact>]
let ``String.Compare with comparison works`` () =
// String.Compare("ABC", "abc", StringComparison.InvariantCulture) > 0 |> equal true
Expand Down Expand Up @@ -846,6 +851,23 @@ let ``String.Contains works`` () =
"ABC".Contains("B") |> equal true
"ABC".Contains("Z") |> equal false

[<Fact>]
let ``String.Contains with char works`` () =
"ABC".Contains('B') |> equal true
"ABC".Contains('Z') |> equal false

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

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

[<Fact>]
let ``String.PadLeft works`` () =
"3.14".PadLeft(10) |> equal " 3.14"
Expand Down

0 comments on commit 4612e59

Please sign in to comment.