Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Rust] Updated string comparisons #3950

Merged
merged 1 commit into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)

## 4.23.0 - 2024-10-28
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
Loading