Skip to content

Commit 2b9e193

Browse files
author
Robert Stam
committed
Implemented support for combining ToLower, ToUpper, Trim, TrimStart or TrimEnd with Contains, StartsWith or EndsWith in LINQ queries.
1 parent 12f9bfc commit 2b9e193

File tree

6 files changed

+390
-42
lines changed

6 files changed

+390
-42
lines changed

Bson/IO/JsonWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,7 @@ public override void WriteRegularExpression(string pattern, string options)
540540
case JsonOutputMode.Shell:
541541
WriteNameHelper(Name);
542542
_textWriter.Write("/");
543-
var escaped = (pattern == "") ? "(?:)" : pattern.Replace(@"\", @"\\").Replace("/", @"\/");
543+
var escaped = (pattern == "") ? "(?:)" : pattern.Replace("/", @"\/");
544544
_textWriter.Write(escaped);
545545
_textWriter.Write("/");
546546
_textWriter.Write(options);

Bson/ObjectModel/BsonRegularExpression.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public BsonRegularExpression(string pattern)
4747
{
4848
var index = pattern.LastIndexOf('/');
4949
var escaped = pattern.Substring(1, index - 1);
50-
var unescaped = (escaped == "(?:)") ? "" : Regex.Replace(escaped, @"\\(.)", "$1");
50+
var unescaped = (escaped == "(?:)") ? "" : escaped.Replace("\\/", "/");
5151
_pattern = unescaped;
5252
_options = pattern.Substring(index + 1);
5353
}
@@ -333,7 +333,7 @@ public Regex ToRegex()
333333
/// <returns>A string representation of the value.</returns>
334334
public override string ToString()
335335
{
336-
var escaped = _pattern.Replace(@"\", @"\\").Replace("/", @"\/");
336+
var escaped = (_pattern == "") ? "(?:)" :_pattern.Replace("/", @"\/");
337337
return string.Format("/{0}/{1}", escaped, _options);
338338
}
339339
}

BsonUnitTests/IO/JsonWriterTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ public void TestRegularExpressionShell()
424424
new TestData<BsonRegularExpression>(BsonRegularExpression.Create(""), "/(?:)/"),
425425
new TestData<BsonRegularExpression>(BsonRegularExpression.Create("a"), "/a/"),
426426
new TestData<BsonRegularExpression>(BsonRegularExpression.Create("a/b"), "/a\\/b/"),
427-
new TestData<BsonRegularExpression>(BsonRegularExpression.Create("a\\b"), "/a\\\\b/"),
427+
new TestData<BsonRegularExpression>(BsonRegularExpression.Create("a\\b"), "/a\\b/"),
428428
new TestData<BsonRegularExpression>(BsonRegularExpression.Create("a", "i"), "/a/i"),
429429
new TestData<BsonRegularExpression>(BsonRegularExpression.Create("a", "m"), "/a/m"),
430430
new TestData<BsonRegularExpression>(BsonRegularExpression.Create("a", "x"), "/a/x"),

Driver/Linq/Expressions/ExpressionFormatter.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,8 +425,8 @@ private void VisitValue(object value)
425425
if (a != null && a.Rank == 1)
426426
{
427427
var elementType = a.GetType().GetElementType();
428-
_sb.AppendFormat("{0}[]:{{ ", elementType.Name);
429-
var separator = "";
428+
_sb.AppendFormat("{0}[]:{{", elementType.Name);
429+
var separator = " ";
430430
foreach (var item in a)
431431
{
432432
_sb.Append(separator);
@@ -443,6 +443,13 @@ private void VisitValue(object value)
443443
return;
444444
}
445445

446+
if (value.GetType() == typeof(char))
447+
{
448+
var c = (char)value;
449+
_sb.AppendFormat("'{0}'", c.ToString());
450+
return;
451+
}
452+
446453
if (value.GetType() == typeof(DateTime))
447454
{
448455
var dt = (DateTime)value;

Driver/Linq/Translators/SelectQuery.cs

Lines changed: 135 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -806,35 +806,103 @@ private IMongoQuery BuildStringLengthQuery(Expression variableExpression, Expres
806806

807807
private IMongoQuery BuildStringQuery(MethodCallExpression methodCallExpression)
808808
{
809-
if (methodCallExpression.Method.DeclaringType == typeof(string))
809+
if (methodCallExpression.Method.DeclaringType != typeof(string))
810810
{
811-
switch (methodCallExpression.Method.Name)
811+
return null;
812+
}
813+
814+
var arguments = methodCallExpression.Arguments.ToArray();
815+
if (arguments.Length != 1)
816+
{
817+
return null;
818+
}
819+
820+
var stringExpression = methodCallExpression.Object;
821+
var constantExpression = arguments[0] as ConstantExpression;
822+
if (constantExpression == null)
823+
{
824+
return null;
825+
}
826+
827+
var pattern = (string)constantExpression.Value; // TODO: escape value
828+
switch (methodCallExpression.Method.Name)
829+
{
830+
case "Contains": pattern = ".*" + pattern + ".*"; break;
831+
case "EndsWith": pattern = ".*" + pattern; break;
832+
case "StartsWith": pattern = pattern + ".*"; break;
833+
default: return null;
834+
}
835+
836+
var caseInsensitive = false;
837+
MethodCallExpression stringMethodCallExpression;
838+
while ((stringMethodCallExpression = stringExpression as MethodCallExpression) != null)
839+
{
840+
var trimStart = false;
841+
var trimEnd = false;
842+
Expression trimCharsExpression = null;
843+
switch (stringMethodCallExpression.Method.Name)
812844
{
813-
case "Contains":
814-
case "EndsWith":
815-
case "StartsWith":
816-
var arguments = methodCallExpression.Arguments.ToArray();
817-
if (arguments.Length == 1)
818-
{
819-
var serializationInfo = GetSerializationInfo(methodCallExpression.Object);
820-
var valueExpression = arguments[0] as ConstantExpression;
821-
if (serializationInfo != null && valueExpression != null)
822-
{
823-
var s = (string)valueExpression.Value;
824-
BsonRegularExpression regex;
825-
switch (methodCallExpression.Method.Name)
826-
{
827-
case "Contains": regex = new BsonRegularExpression(s); break;
828-
case "EndsWith": regex = new BsonRegularExpression(s + "$"); break;
829-
case "StartsWith": regex = new BsonRegularExpression("^" + s); break;
830-
default: throw new InvalidOperationException("Unreachable code");
831-
}
832-
return Query.Matches(serializationInfo.ElementName, regex);
833-
}
834-
}
845+
case "ToLower":
846+
caseInsensitive = true;
847+
break;
848+
case "ToUpper":
849+
caseInsensitive = true;
835850
break;
851+
case "Trim":
852+
trimStart = true;
853+
trimEnd = true;
854+
trimCharsExpression = stringMethodCallExpression.Arguments.FirstOrDefault();
855+
break;
856+
case "TrimEnd":
857+
trimEnd = true;
858+
trimCharsExpression = stringMethodCallExpression.Arguments.First();
859+
break;
860+
case "TrimStart":
861+
trimStart = true;
862+
trimCharsExpression = stringMethodCallExpression.Arguments.First();
863+
break;
864+
default:
865+
return null;
836866
}
867+
868+
if (trimStart || trimEnd)
869+
{
870+
var trimCharsPattern = GetTrimCharsPattern(trimCharsExpression);
871+
if (trimCharsPattern == null)
872+
{
873+
return null;
874+
}
875+
876+
if (trimStart)
877+
{
878+
pattern = trimCharsPattern + pattern;
879+
}
880+
if (trimEnd)
881+
{
882+
pattern = pattern + trimCharsPattern;
883+
}
884+
}
885+
886+
stringExpression = stringMethodCallExpression.Object;
887+
}
888+
889+
pattern = "^" + pattern + "$";
890+
if (pattern.StartsWith("^.*"))
891+
{
892+
pattern = pattern.Substring(3);
893+
}
894+
if (pattern.EndsWith(".*$"))
895+
{
896+
pattern = pattern.Substring(0, pattern.Length - 3);
837897
}
898+
899+
var serializationInfo = GetSerializationInfo(stringExpression);
900+
if (serializationInfo != null)
901+
{
902+
var options = caseInsensitive ? "is" : "s";
903+
return Query.Matches(serializationInfo.ElementName, new BsonRegularExpression(pattern, options));
904+
}
905+
838906
return null;
839907
}
840908

@@ -1004,6 +1072,49 @@ private BsonSerializationInfo GetSerializationInfoMember(IBsonSerializer seriali
10041072
}
10051073
}
10061074

1075+
private string GetTrimCharsPattern(Expression trimCharsExpression)
1076+
{
1077+
if (trimCharsExpression == null)
1078+
{
1079+
return "\\s*";
1080+
}
1081+
1082+
var constantExpresion = trimCharsExpression as ConstantExpression;
1083+
if (constantExpresion == null || constantExpresion.Type != typeof(char[]))
1084+
{
1085+
return null;
1086+
}
1087+
1088+
var trimChars = (char[])constantExpresion.Value;
1089+
if (trimChars.Length == 0)
1090+
{
1091+
return "\\s*";
1092+
}
1093+
1094+
// build a pattern that matches the characters to be trimmed
1095+
var sb = new StringBuilder();
1096+
sb.Append("[");
1097+
var sawDash = false; // if dash is one of the characters it must be last in the pattern
1098+
foreach (var c in trimChars)
1099+
{
1100+
if (c == '-')
1101+
{
1102+
sawDash = true;
1103+
}
1104+
else
1105+
{
1106+
// TODO: handle special characters better
1107+
sb.Append(c.ToString());
1108+
}
1109+
}
1110+
if (sawDash)
1111+
{
1112+
sb.Append("-");
1113+
}
1114+
sb.Append("]*");
1115+
return sb.ToString();
1116+
}
1117+
10071118
private BsonValue SerializeValue(BsonSerializationInfo serializationInfo, object value)
10081119
{
10091120
var bsonDocument = new BsonDocument();

0 commit comments

Comments
 (0)