Skip to content

Commit 5bc46e3

Browse files
committed
add more expressions
1 parent 617feb4 commit 5bc46e3

File tree

3 files changed

+194
-11
lines changed

3 files changed

+194
-11
lines changed

Firestore/Swift/Source/ExpressionImplementation.swift

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -336,10 +336,10 @@ extension Expression {
336336
/// Field("content").stringReplace(Field("target"), Field("replacement"))
337337
/// ```
338338
///
339-
/// - Parameter find: An `Expr` (evaluating to a string) for the substring to search for.
340-
/// - Parameter replace: An `Expr` (evaluating to a string) for the substring to replace all
339+
/// - Parameter find: An `Expression` (evaluating to a string) for the substring to search for.
340+
/// - Parameter replace: An `Expression` (evaluating to a string) for the substring to replace all
341341
/// occurrences with.
342-
/// - Returns: A new `FunctionExpr` representing the string with all occurrences replaced.
342+
/// - Returns: A new `FunctionExpression` representing the string with all occurrences replaced.
343343
func stringReplace(_ find: Expression, with replace: Expression) -> FunctionExpression{
344344
return FunctionExpression("string_replace", [self, find, replace])
345345
}
@@ -377,6 +377,14 @@ public extension Expression {
377377
return FunctionExpression("pow", [self, exponent])
378378
}
379379

380+
func round() -> FunctionExpression {
381+
return FunctionExpression("round", [self])
382+
}
383+
384+
func sqrt() -> FunctionExpression {
385+
return FunctionExpression("sqrt", [self])
386+
}
387+
380388
func exp() -> FunctionExpression {
381389
return FunctionExpression("exp", [self])
382390
}
@@ -623,11 +631,11 @@ public extension Expression {
623631
return BooleanExpression("regex_match", [self, pattern])
624632
}
625633

626-
func strContains(_ substring: String) -> BooleanExpression {
634+
func stringContains(_ substring: String) -> BooleanExpression {
627635
return BooleanExpression("string_contains", [self, Helper.sendableToExpr(substring)])
628636
}
629637

630-
func string_contains(_ expression: Expression) -> BooleanExpression {
638+
func stringContains(_ expression: Expression) -> BooleanExpression {
631639
return BooleanExpression("string_contains", [self, expression])
632640
}
633641

@@ -663,10 +671,19 @@ public extension Expression {
663671
return FunctionExpression("string_concat", [self] + strings)
664672
}
665673

674+
func stringConcat(_ strings: [Sendable]) -> FunctionExpression {
675+
let exprs = [self] + strings.map { Helper.sendableToExpr($0) }
676+
return FunctionExpression("string_concat", exprs)
677+
}
678+
666679
func reverse() -> FunctionExpression {
667680
return FunctionExpression("reverse", [self])
668681
}
669682

683+
func stringReverse() -> FunctionExpression {
684+
return FunctionExpression("string_reverse", [self])
685+
}
686+
670687
func byteLength() -> FunctionExpression {
671688
return FunctionExpression("byte_length", [self])
672689
}

Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/Expression.swift

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,26 @@ public protocol Expression: Sendable {
3636

3737
// --- Added Mathematical Operations ---
3838

39+
/// Creates an expression that returns the value of self rounded to the nearest integer.
40+
///
41+
/// ```swift
42+
/// // Get the value of the "amount" field rounded to the nearest integer.
43+
/// Field("amount").round()
44+
/// ```
45+
///
46+
/// - Returns: A new `FunctionExpression` representing the rounded number.
47+
func round() -> FunctionExpression
48+
49+
/// Creates an expression that returns the square root of self.
50+
///
51+
/// ```swift
52+
/// // Get the square root of the "area" field.
53+
/// Field("area").sqrt()
54+
/// ```
55+
///
56+
/// - Returns: A new `FunctionExpression` representing the square root of the number.
57+
func sqrt() -> FunctionExpression
58+
3959
/// Creates an expression that returns the value of self raised to the power of Y.
4060
///
4161
/// Returns zero on underflow.
@@ -797,26 +817,26 @@ public protocol Expression: Sendable {
797817
///
798818
/// ```swift
799819
/// // Check if the "description" field contains "example".
800-
/// Field("description").string_contains("example")
820+
/// Field("description").stringContains("example")
801821
/// ```
802822
///
803823
/// - Parameter substring: The literal string substring to search for.
804-
/// - Returns: A new `BooleanExpr` representing the "str_contains" comparison.
805-
func strContains(_ substring: String) -> BooleanExpression
824+
/// - Returns: A new `BooleanExpr` representing the "stringContains" comparison.
825+
func stringContains(_ substring: String) -> BooleanExpression
806826

807827
/// Creates an expression that checks if a string (from `self`) contains a specified substring
808828
/// from an expression (case-sensitive).
809829
/// Assumes `self` evaluates to a string, and `expr` evaluates to a string.
810830
///
811831
/// ```swift
812832
/// // Check if the "message" field contains the value of the "keyword" field.
813-
/// Field("message").string_contains(Field("keyword"))
833+
/// Field("message").stringContains(Field("keyword"))
814834
/// ```
815835
///
816836
/// - Parameter expr: An `Expression` (evaluating to a string) representing the substring to
817837
/// search for.
818838
/// - Returns: A new `BooleanExpr` representing the "str_contains" comparison.
819-
func string_contains(_ expression: Expression) -> BooleanExpression
839+
func stringContains(_ expression: Expression) -> BooleanExpression
820840

821841
/// Creates an expression that checks if a string (from `self`) starts with a given literal prefix
822842
/// (case-sensitive).
@@ -906,6 +926,18 @@ public protocol Expression: Sendable {
906926
/// - Returns: A new `FunctionExpression` representing the trimmed string.
907927
func trim() -> FunctionExpression
908928

929+
/// Creates an expression that concatenates this string expression with other string expressions.
930+
/// Assumes `self` and all parameters evaluate to strings.
931+
///
932+
/// ```swift
933+
/// // Combine "firstName", " ", and "lastName"
934+
/// Field("firstName").stringConcat([" ", Field("lastName")])
935+
/// ```
936+
///
937+
/// - Parameter strings: An array of `Expression` or `String` to concatenate.
938+
/// - Returns: A new `FunctionExpression` representing the concatenated string.
939+
func stringConcat(_ strings: [Sendable]) -> FunctionExpression
940+
909941
/// Creates an expression that concatenates this string expression with other string expressions.
910942
/// Assumes `self` and all parameters evaluate to strings.
911943
///
@@ -931,6 +963,17 @@ public protocol Expression: Sendable {
931963
/// - Returns: A new `FunctionExpr` representing the reversed string.
932964
func reverse() -> FunctionExpression
933965

966+
/// Creates an expression that reverses this string expression.
967+
/// Assumes `self` evaluates to a string.
968+
///
969+
/// ```swift
970+
/// // Reverse the value of the "myString" field.
971+
/// Field("myString").stringReverse()
972+
/// ```
973+
///
974+
/// - Returns: A new `FunctionExpr` representing the reversed string.
975+
func stringReverse() -> FunctionExpression
976+
934977
/// Creates an expression that calculates the length of this string or bytes expression in bytes.
935978
/// Assumes `self` evaluates to a string or bytes.
936979
///

Firestore/Swift/Tests/Integration/PipelineTests.swift

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1831,6 +1831,25 @@ class PipelineIntegrationTests: FSTIntegrationTestCase {
18311831
TestHelper.compare(pipelineSnapshot: snapshot, expected: expectedResults, enforceOrder: true)
18321832
}
18331833

1834+
func testStringConcatWithSendable() async throws {
1835+
let collRef = collectionRef(withDocuments: bookDocs)
1836+
let db = collRef.firestore
1837+
1838+
let pipeline = db.pipeline()
1839+
.collection(collRef.path)
1840+
.sort([Field("author").ascending()])
1841+
.select([Field("author").stringConcat([" - ", Field("title")]).as("bookInfo")])
1842+
.limit(1)
1843+
1844+
let snapshot = try await pipeline.execute()
1845+
1846+
let expectedResults: [[String: Sendable]] = [
1847+
["bookInfo": "Douglas Adams - The Hitchhiker's Guide to the Galaxy"],
1848+
]
1849+
1850+
TestHelper.compare(pipelineSnapshot: snapshot, expected: expectedResults, enforceOrder: true)
1851+
}
1852+
18341853
func testStartsWith() async throws {
18351854
let collRef = collectionRef(withDocuments: bookDocs)
18361855
let db = collRef.firestore
@@ -1879,7 +1898,7 @@ class PipelineIntegrationTests: FSTIntegrationTestCase {
18791898

18801899
let pipeline = db.pipeline()
18811900
.collection(collRef.path)
1882-
.where(Field("title").strContains("'s"))
1901+
.where(Field("title").stringContains("'s"))
18831902
.select(["title"])
18841903
.sort([Field("title").ascending()])
18851904

@@ -1944,6 +1963,58 @@ class PipelineIntegrationTests: FSTIntegrationTestCase {
19441963
TestHelper.compare(pipelineSnapshot: snapshot, expected: expectedResults, enforceOrder: true)
19451964
}
19461965

1966+
func testReverseWorksOnString() async throws {
1967+
let collRef = collectionRef(withDocuments: [
1968+
"doc1": ["value": "abc"],
1969+
"doc2": ["value": ""],
1970+
"doc3": ["value": "a"],
1971+
])
1972+
let db = collRef.firestore
1973+
1974+
let pipeline = db.pipeline()
1975+
.collection(collRef.path)
1976+
.select([
1977+
Field("value").reverse().as("reversedValue"),
1978+
])
1979+
.sort([Field("reversedValue").ascending()])
1980+
1981+
let snapshot = try await pipeline.execute()
1982+
1983+
let expectedResults: [[String: Sendable]] = [
1984+
["reversedValue": ""],
1985+
["reversedValue": "a"],
1986+
["reversedValue": "cba"],
1987+
]
1988+
1989+
TestHelper.compare(pipelineSnapshot: snapshot, expected: expectedResults, enforceOrder: true)
1990+
}
1991+
1992+
func testReverseWorksOnArray() async throws {
1993+
let collRef = collectionRef(withDocuments: [
1994+
"doc1": ["tags": ["a", "b", "c"]],
1995+
"doc2": ["tags": [1, 2, 3]],
1996+
"doc3": ["tags": []],
1997+
])
1998+
let db = collRef.firestore
1999+
2000+
let pipeline = db.pipeline()
2001+
.collection(collRef.path)
2002+
.select([
2003+
Field("tags").reverse().as("reversedTags"),
2004+
])
2005+
.sort([Field("reversedTags").ascending()])
2006+
2007+
let snapshot = try await pipeline.execute()
2008+
2009+
let expectedResults: [[String: Sendable]] = [
2010+
["reversedTags": []],
2011+
["reversedTags": [3, 2, 1]],
2012+
["reversedTags": ["c", "b", "a"]],
2013+
]
2014+
2015+
TestHelper.compare(pipelineSnapshot: snapshot, expected: expectedResults, enforceOrder: true)
2016+
}
2017+
19472018
func testLike() async throws {
19482019
let collRef = collectionRef(withDocuments: bookDocs)
19492020
let db = collRef.firestore
@@ -2156,6 +2227,58 @@ class PipelineIntegrationTests: FSTIntegrationTestCase {
21562227
TestHelper.compare(pipelineSnapshot: snapshot, expected: expectedResults, enforceOrder: true)
21572228
}
21582229

2230+
func testRoundWorks() async throws {
2231+
let collRef = collectionRef(withDocuments: [
2232+
"doc1": ["value": -10.8],
2233+
"doc2": ["value": 5.3],
2234+
"doc3": ["value": 0],
2235+
])
2236+
let db = collRef.firestore
2237+
2238+
let pipeline = db.pipeline()
2239+
.collection(collRef.path)
2240+
.select([
2241+
Field("value").round().as("roundValue"),
2242+
])
2243+
.sort([Field("roundValue").ascending()])
2244+
2245+
let snapshot = try await pipeline.execute()
2246+
2247+
let expectedResults: [[String: Sendable]] = [
2248+
["roundValue": -11],
2249+
["roundValue": 0],
2250+
["roundValue": 5],
2251+
]
2252+
2253+
TestHelper.compare(pipelineSnapshot: snapshot, expected: expectedResults, enforceOrder: true)
2254+
}
2255+
2256+
func testSqrtWorks() async throws {
2257+
let collRef = collectionRef(withDocuments: [
2258+
"doc1": ["value": 4],
2259+
"doc2": ["value": 9],
2260+
"doc3": ["value": 16],
2261+
])
2262+
let db = collRef.firestore
2263+
2264+
let pipeline = db.pipeline()
2265+
.collection(collRef.path)
2266+
.select([
2267+
Field("value").sqrt().as("sqrtValue"),
2268+
])
2269+
.sort([Field("sqrtValue").ascending()])
2270+
2271+
let snapshot = try await pipeline.execute()
2272+
2273+
let expectedResults: [[String: Sendable]] = [
2274+
["sqrtValue": 2],
2275+
["sqrtValue": 3],
2276+
["sqrtValue": 4],
2277+
]
2278+
2279+
TestHelper.compare(pipelineSnapshot: snapshot, expected: expectedResults, enforceOrder: true)
2280+
}
2281+
21592282
func testExpWorks() async throws {
21602283
let collRef = collectionRef(withDocuments: [
21612284
"doc1": ["value": 1],

0 commit comments

Comments
 (0)