Skip to content

Commit 2e68d65

Browse files
committed
Implement and test realtime string functions
1 parent 2cb2f45 commit 2e68d65

File tree

3 files changed

+1096
-31
lines changed

3 files changed

+1096
-31
lines changed

firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt

Lines changed: 150 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import com.google.firestore.v1.Value
1818
import com.google.firestore.v1.Value.ValueTypeCase
1919
import com.google.protobuf.ByteString
2020
import com.google.protobuf.Timestamp
21+
import com.google.re2j.Pattern
22+
import com.google.re2j.PatternSyntaxException
2123
import java.math.BigDecimal
2224
import java.math.RoundingMode
2325
import kotlin.math.absoluteValue
@@ -363,6 +365,10 @@ internal val evaluateStrConcat = variadicFunction { strings: List<String> ->
363365
EvaluateResult.string(buildString { strings.forEach(::append) })
364366
}
365367

368+
internal val evaluateStrContains = binaryFunction { value: String, substring: String ->
369+
EvaluateResult.boolean(value.contains(substring))
370+
}
371+
366372
internal val evaluateStartsWith = binaryFunction { value: String, prefix: String ->
367373
EvaluateResult.boolean(value.startsWith(prefix))
368374
}
@@ -385,24 +391,81 @@ internal val evaluateCharLength = unaryFunction { s: String ->
385391
EvaluateResult.long(s.codePointCount(0, s.length))
386392
}
387393

388-
internal val evaluateToLowercase = notImplemented
394+
internal val evaluateToLowercase = unaryFunctionPrimitive(String::lowercase)
389395

390-
internal val evaluateToUppercase = notImplemented
396+
internal val evaluateToUppercase = unaryFunctionPrimitive(String::uppercase)
391397

392-
internal val evaluateReverse = notImplemented
398+
internal val evaluateReverse = unaryFunctionPrimitive(String::reversed)
393399

394400
internal val evaluateSplit = notImplemented // TODO: Does not exist in expressions.kt yet.
395401

396402
internal val evaluateSubstring = notImplemented // TODO: Does not exist in expressions.kt yet.
397403

398-
internal val evaluateTrim = notImplemented
404+
internal val evaluateTrim = unaryFunctionPrimitive(String::trim)
399405

400406
internal val evaluateLTrim = notImplemented // TODO: Does not exist in expressions.kt yet.
401407

402408
internal val evaluateRTrim = notImplemented // TODO: Does not exist in expressions.kt yet.
403409

404410
internal val evaluateStrJoin = notImplemented // TODO: Does not exist in expressions.kt yet.
405411

412+
internal val evaluateReplaceAll = notImplemented // TODO: Does not exist in backend yet.
413+
414+
internal val evaluateReplaceFirst = notImplemented // TODO: Does not exist in backend yet.
415+
416+
internal val evaluateRegexContains = binaryPatternFunction { pattern: Pattern, value: String ->
417+
pattern.matcher(value).find()
418+
}
419+
420+
internal val evaluateRegexMatch = binaryPatternFunction(Pattern::matches)
421+
422+
internal val evaluateLike =
423+
binaryPatternConstructorFunction(
424+
{ likeString: String ->
425+
try {
426+
Pattern.compile(likeToRegex(likeString))
427+
} catch (e: Exception) {
428+
null
429+
}
430+
},
431+
Pattern::matches
432+
)
433+
434+
private fun likeToRegex(like: String): String = buildString {
435+
var escape = false
436+
for (c in like) {
437+
if (escape) {
438+
escape = false
439+
when (c) {
440+
'\\' -> append("\\\\")
441+
else -> append(c)
442+
}
443+
} else
444+
when (c) {
445+
'\\' -> escape = true
446+
'_' -> append('.')
447+
'%' -> append(".*")
448+
'.' -> append("\\.")
449+
'*' -> append("\\*")
450+
'?' -> append("\\?")
451+
'+' -> append("\\+")
452+
'^' -> append("\\^")
453+
'$' -> append("\\$")
454+
'|' -> append("\\|")
455+
'(' -> append("\\(")
456+
')' -> append("\\)")
457+
'[' -> append("\\[")
458+
']' -> append("\\]")
459+
'{' -> append("\\{")
460+
'}' -> append("\\}")
461+
else -> append(c)
462+
}
463+
}
464+
if (escape) {
465+
throw Exception("LIKE pattern ends in backslash")
466+
}
467+
}
468+
406469
// === Date / Timestamp Functions ===
407470

408471
private const val L_NANOS_PER_SECOND: Long = 1000_000_000
@@ -574,6 +637,12 @@ private inline fun unaryFunction(crossinline stringOp: (Boolean) -> EvaluateResu
574637
stringOp,
575638
)
576639

640+
@JvmName("unaryStringFunctionPrimitive")
641+
private inline fun unaryFunctionPrimitive(crossinline stringOp: (String) -> String) =
642+
unaryFunction { s: String ->
643+
EvaluateResult.string(stringOp(s))
644+
}
645+
577646
@JvmName("unaryStringFunction")
578647
private inline fun unaryFunction(crossinline stringOp: (String) -> EvaluateResult) =
579648
unaryFunctionType(
@@ -696,6 +765,49 @@ private inline fun binaryFunction(crossinline function: (String, String) -> Eval
696765
function
697766
)
698767

768+
@JvmName("binaryStringPatternConstructorFunction")
769+
private inline fun binaryPatternConstructorFunction(
770+
crossinline patternConstructor: (String) -> Pattern?,
771+
crossinline function: (Pattern, String) -> Boolean
772+
) =
773+
binaryFunctionConstructorType(
774+
ValueTypeCase.STRING_VALUE,
775+
Value::getStringValue,
776+
ValueTypeCase.STRING_VALUE,
777+
Value::getStringValue
778+
) {
779+
val cache = cache(patternConstructor)
780+
({ value: String, regex: String ->
781+
val pattern = cache(regex)
782+
if (pattern == null) EvaluateResultError else EvaluateResult.boolean(function(pattern, value))
783+
})
784+
}
785+
786+
@JvmName("binaryStringPatternFunction")
787+
private inline fun binaryPatternFunction(crossinline function: (Pattern, String) -> Boolean) =
788+
binaryPatternConstructorFunction(
789+
{ s: String ->
790+
try {
791+
Pattern.compile(s)
792+
} catch (e: PatternSyntaxException) {
793+
null
794+
}
795+
},
796+
function
797+
)
798+
799+
private inline fun <T> cache(crossinline ifAbsent: (String) -> T): (String) -> T? {
800+
var cache: Pair<String?, T?> = Pair(null, null)
801+
return block@{ s: String ->
802+
var (regex, pattern) = cache
803+
if (regex != s) {
804+
pattern = ifAbsent(s)
805+
cache = Pair(s, pattern)
806+
}
807+
return@block pattern
808+
}
809+
}
810+
699811
@JvmName("binaryArrayArrayFunction")
700812
private inline fun binaryFunction(
701813
crossinline function: (List<Value>, List<Value>) -> EvaluateResult
@@ -756,12 +868,43 @@ private inline fun <T1, T2> binaryFunctionType(
756868
valueTypeCase2: ValueTypeCase,
757869
crossinline valueExtractor2: (Value) -> T2,
758870
crossinline function: (T1, T2) -> EvaluateResult
871+
): EvaluateFunction = { params ->
872+
if (params.size != 2)
873+
throw Assert.fail("Function should have exactly 2 params, but %d were given.", params.size)
874+
(block@{ input: MutableDocument ->
875+
val v1 = params[0](input).value ?: return@block EvaluateResultError
876+
val v2 = params[1](input).value ?: return@block EvaluateResultError
877+
when (v1.valueTypeCase) {
878+
ValueTypeCase.NULL_VALUE ->
879+
when (v2.valueTypeCase) {
880+
ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL
881+
valueTypeCase2 -> EvaluateResult.NULL
882+
else -> EvaluateResultError
883+
}
884+
valueTypeCase1 ->
885+
when (v2.valueTypeCase) {
886+
ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL
887+
valueTypeCase2 -> catch { function(valueExtractor1(v1), valueExtractor2(v2)) }
888+
else -> EvaluateResultError
889+
}
890+
else -> EvaluateResultError
891+
}
892+
})
893+
}
894+
895+
private inline fun <T1, T2> binaryFunctionConstructorType(
896+
valueTypeCase1: ValueTypeCase,
897+
crossinline valueExtractor1: (Value) -> T1,
898+
valueTypeCase2: ValueTypeCase,
899+
crossinline valueExtractor2: (Value) -> T2,
900+
crossinline functionConstructor: () -> (T1, T2) -> EvaluateResult
759901
): EvaluateFunction = { params ->
760902
if (params.size != 2)
761903
throw Assert.fail("Function should have exactly 2 params, but %d were given.", params.size)
762904
val p1 = params[0]
763905
val p2 = params[1]
764-
block@{ input: MutableDocument ->
906+
val f = functionConstructor()
907+
(block@{ input: MutableDocument ->
765908
val v1 = p1(input).value ?: return@block EvaluateResultError
766909
val v2 = p2(input).value ?: return@block EvaluateResultError
767910
when (v1.valueTypeCase) {
@@ -774,12 +917,12 @@ private inline fun <T1, T2> binaryFunctionType(
774917
valueTypeCase1 ->
775918
when (v2.valueTypeCase) {
776919
ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL
777-
valueTypeCase2 -> catch { function(valueExtractor1(v1), valueExtractor2(v2)) }
920+
valueTypeCase2 -> catch { f(valueExtractor1(v1), valueExtractor2(v2)) }
778921
else -> EvaluateResultError
779922
}
780923
else -> EvaluateResultError
781924
}
782-
}
925+
})
783926
}
784927

785928
private inline fun variadicResultFunction(

0 commit comments

Comments
 (0)