@@ -18,6 +18,8 @@ import com.google.firestore.v1.Value
1818import com.google.firestore.v1.Value.ValueTypeCase
1919import com.google.protobuf.ByteString
2020import com.google.protobuf.Timestamp
21+ import com.google.re2j.Pattern
22+ import com.google.re2j.PatternSyntaxException
2123import java.math.BigDecimal
2224import java.math.RoundingMode
2325import 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+
366372internal 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
394400internal val evaluateSplit = notImplemented // TODO: Does not exist in expressions.kt yet.
395401
396402internal 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
400406internal val evaluateLTrim = notImplemented // TODO: Does not exist in expressions.kt yet.
401407
402408internal val evaluateRTrim = notImplemented // TODO: Does not exist in expressions.kt yet.
403409
404410internal 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
408471private 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" )
578647private 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" )
700812private 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
785928private inline fun variadicResultFunction (
0 commit comments