Skip to content

Commit f5ceac5

Browse files
authored
Merge pull request #21 from jamescurran/master
We need an automated build at some point
2 parents 5dba2d4 + 93d3f4d commit f5ceac5

File tree

5 files changed

+244
-29
lines changed

5 files changed

+244
-29
lines changed

MicroRuleEngine.Tests/MicroRuleEngine.Tests.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,15 @@
5555
</CodeAnalysisDependentAssemblyPaths>
5656
</ItemGroup>
5757
<ItemGroup>
58+
<Compile Include="ExceptionTests.cs" />
5859
<Compile Include="IsTypeTests.cs" />
5960
<Compile Include="NewAPI.cs" />
6061
<Compile Include="Models\Order.cs" />
6162
<Compile Include="Properties\AssemblyInfo.cs" />
6263
<Compile Include="ExampleUsage.cs" />
6364
<Compile Include="DataRowTest.cs" />
6465
<Compile Include="SerializationTests.cs" />
65-
<Compile Include="ExceptionTests.cs" />
66+
<Compile Include="TimeTest.cs" />
6667
</ItemGroup>
6768
<ItemGroup>
6869
<ProjectReference Include="..\MicroRuleEngine\MicroRuleEngine.csproj">

MicroRuleEngine.Tests/NewAPI.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using Microsoft.VisualStudio.TestTools.UnitTesting;
45
using MicroRuleEngine.Tests.Models;
@@ -177,6 +178,47 @@ public void ListTest()
177178
Assert.IsFalse(passes);
178179
}
179180

181+
class DictTest<T>
182+
{
183+
public Dictionary<T, int> Dict { get; set; }
184+
}
185+
186+
[TestMethod]
187+
public void Dictionary_StringIndex()
188+
{
189+
var objDict = new DictTest<string> { Dict = new Dictionary<string, int>()};
190+
objDict.Dict.Add("Key", 1234);
191+
192+
Rule rule = Rule.Create("Dict['Key']", mreOperator.Equal, 1234);
193+
194+
MRE engine = new MRE();
195+
var compiledRule = engine.CompileRule<DictTest<string>>(rule);
196+
bool passes = compiledRule(objDict);
197+
Assert.IsTrue(passes);
198+
199+
objDict.Dict["Key"] = 2345;
200+
passes = compiledRule(objDict);
201+
Assert.IsFalse(passes);
202+
}
203+
204+
[TestMethod]
205+
public void Dictionary_IntIndex()
206+
{
207+
var objDict = new DictTest<int> { Dict = new Dictionary<int, int>() };
208+
objDict.Dict.Add(111, 1234);
209+
210+
Rule rule = Rule.Create("Dict[111]", mreOperator.Equal, 1234);
211+
212+
MRE engine = new MRE();
213+
var compiledRule = engine.CompileRule<DictTest<int>>(rule);
214+
bool passes = compiledRule(objDict);
215+
Assert.IsTrue(passes);
216+
217+
objDict.Dict[111] = 2345;
218+
passes = compiledRule(objDict);
219+
Assert.IsFalse(passes);
220+
}
221+
180222
[TestMethod]
181223
public void SelfReferenialTest()
182224
{

MicroRuleEngine.Tests/TimeTest.cs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using System;
2+
using MicroRuleEngine.Tests.Models;
3+
using Microsoft.VisualStudio.TestTools.UnitTesting;
4+
5+
namespace MicroRuleEngine.Tests
6+
{
7+
[TestClass]
8+
public class TimeTest
9+
{
10+
[TestMethod]
11+
public void Time_InRange_Minutes()
12+
{
13+
Order order = ExampleUsage.GetOrder();
14+
Rule rule = Rule.Create("OrderDate", mreOperator.GreaterThanOrEqual, "#NOW-90M");
15+
order.OrderDate = DateTime.Now.AddMinutes(-60);
16+
17+
MRE engine = new MRE();
18+
var boolMethod = engine.CompileRule<Order>(rule);
19+
bool passes = boolMethod(order);
20+
Assert.IsTrue(passes);
21+
22+
}
23+
24+
[TestMethod]
25+
public void Time_OutOfRange_Minutes()
26+
{
27+
Order order = ExampleUsage.GetOrder();
28+
Rule rule = Rule.Create("OrderDate", mreOperator.GreaterThanOrEqual, "#NOW-90M");
29+
order.OrderDate = DateTime.Now.AddMinutes(-100);
30+
31+
MRE engine = new MRE();
32+
var boolMethod = engine.CompileRule<Order>(rule);
33+
bool passes = boolMethod(order);
34+
Assert.IsFalse(passes);
35+
36+
}
37+
38+
[TestMethod]
39+
[ExpectedException(typeof(FormatException))] // TODO: Make throw RuleException
40+
public void Time_BadTarget()
41+
{
42+
Order order = ExampleUsage.GetOrder();
43+
Rule rule = Rule.Create("OrderId", mreOperator.GreaterThanOrEqual, "#NOW-90M");
44+
order.OrderDate = DateTime.Now.AddMinutes(-100);
45+
46+
MRE engine = new MRE();
47+
var boolMethod = engine.CompileRule<Order>(rule);
48+
bool passes = boolMethod(order);
49+
Assert.IsFalse(passes);
50+
}
51+
52+
[TestMethod]
53+
[ExpectedException(typeof(FormatException))] // TODO: Make throw RuleException
54+
public void Time_BadDateString()
55+
{
56+
Order order = ExampleUsage.GetOrder();
57+
Rule rule = Rule.Create("OrderDate", mreOperator.GreaterThanOrEqual, "#NOW*90M");
58+
order.OrderDate = DateTime.Now.AddMinutes(-100);
59+
60+
MRE engine = new MRE();
61+
var boolMethod = engine.CompileRule<Order>(rule);
62+
bool passes = boolMethod(order);
63+
Assert.IsFalse(passes);
64+
65+
}
66+
67+
[TestMethod]
68+
public void Time_InRange_Days()
69+
{
70+
Order order = ExampleUsage.GetOrder();
71+
Rule rule = Rule.Create("OrderDate", mreOperator.GreaterThanOrEqual, "#NOW-90D");
72+
order.OrderDate = DateTime.Now.AddDays(-60);
73+
74+
MRE engine = new MRE();
75+
var boolMethod = engine.CompileRule<Order>(rule);
76+
bool passes = boolMethod(order);
77+
Assert.IsTrue(passes);
78+
}
79+
80+
[TestMethod]
81+
public void Time_OutOfRange_Days()
82+
{
83+
Order order = ExampleUsage.GetOrder();
84+
Rule rule = Rule.Create("OrderDate", mreOperator.GreaterThanOrEqual, "#NOW-90D");
85+
order.OrderDate = DateTime.Now.AddDays(-100);
86+
87+
MRE engine = new MRE();
88+
var boolMethod = engine.CompileRule<Order>(rule);
89+
bool passes = boolMethod(order);
90+
Assert.IsFalse(passes);
91+
92+
}
93+
94+
95+
}
96+
}

MicroRuleEngine/MRE.cs

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ protected static Expression BinaryExpression(IEnumerable<Expression> expressions
133133
return expressions.Aggregate(methodExp);
134134
}
135135

136-
static readonly Regex _regexIndexed = new Regex(@"(\w+)\[(\d+)\]", RegexOptions.Compiled);
136+
private static readonly Regex _regexIndexed =
137+
new Regex(@"(?'Collection'\w+)\[(?:(?'Index'\d+)|(?:['""](?'Key'\w+)[""']))\]", RegexOptions.Compiled);
137138

138139
private static Expression GetProperty(Expression param, string propname)
139140
{
@@ -146,26 +147,38 @@ private static Expression GetProperty(Expression param, string propname)
146147
var isIndexed = _regexIndexed.Match(childprop);
147148
if (isIndexed.Success)
148149
{
149-
var collectionname = isIndexed.Groups[1].Value;
150-
var index = Int32.Parse(isIndexed.Groups[2].Value);
150+
var indexType = typeof(int);
151+
var collectionname = isIndexed.Groups["Collection"].Value;
151152
var collectionProp = propertyType.GetProperty(collectionname);
152153
if (collectionProp == null)
153-
throw new RulesException(
154-
$"Cannot find collection property {collectionname} in class {propertyType.Name} (\"{propname}\")");
154+
throw new RulesException(
155+
$"Cannot find collection property {collectionname} in class {propertyType.Name} (\"{propname}\")");
155156
var collexpr = Expression.PropertyOrField(propExpression, collectionname);
156157

158+
Expression expIndex;
159+
if (isIndexed.Groups["Index"].Success)
160+
{
161+
var index = Int32.Parse(isIndexed.Groups["Index"].Value);
162+
expIndex = Expression.Constant(index);
163+
}
164+
else
165+
{
166+
expIndex = Expression.Constant(isIndexed.Groups["Key"].Value);
167+
indexType = typeof(string);
168+
}
169+
157170
var collectionType = collexpr.Type;
158171
if (collectionType.IsArray)
159172
{
160-
propExpression = Expression.ArrayAccess(collexpr, Expression.Constant(index));
173+
propExpression = Expression.ArrayAccess(collexpr, expIndex);
161174
propertyType = propExpression.Type;
162175
}
163176
else
164177
{
165-
var getter = collectionType.GetMethod("get_Item", new Type[] { typeof(Int32) });
178+
var getter = collectionType.GetMethod("get_Item", new Type[] { indexType });
166179
if (getter == null)
167180
throw new RulesException($"'{collectionname} ({collectionType.Name}) cannot be indexed");
168-
propExpression = Expression.Call(collexpr, getter, Expression.Constant(index));
181+
propExpression = Expression.Call(collexpr, getter, expIndex);
169182
propertyType = getter.ReturnType;
170183
}
171184
}
@@ -412,25 +425,58 @@ private static Expression StringToExpression(object value, Type propType)
412425
safevalue = null;
413426
else if (propType.IsEnum)
414427
safevalue = Enum.Parse(propType, txt);
415-
else if (propType.Name == "Nullable`1")
428+
else
416429
{
417-
valuetype = Nullable.GetUnderlyingType(propType);
418-
safevalue = Convert.ChangeType(value, valuetype);
430+
if (propType.Name == "Nullable`1")
431+
valuetype = Nullable.GetUnderlyingType(propType);
432+
433+
safevalue = IsTime(txt, propType) ?? Convert.ChangeType(value, valuetype);
419434
}
420-
else
421-
safevalue = Convert.ChangeType(value, valuetype);
422435
}
423-
else if (propType.Name == "Nullable`1")
436+
else
424437
{
425-
valuetype = Nullable.GetUnderlyingType(propType);
438+
if (propType.Name == "Nullable`1")
439+
valuetype = Nullable.GetUnderlyingType(propType);
426440
safevalue = Convert.ChangeType(value, valuetype);
427441
}
428-
else
429-
safevalue = Convert.ChangeType(value, valuetype);
430442

431443
return Expression.Constant(safevalue, propType);
432444
}
433445

446+
private static readonly Regex reNow = new Regex(@"#NOW([-+])(\d+)([SMHDY])", RegexOptions.IgnoreCase
447+
| RegexOptions.Compiled
448+
| RegexOptions.Singleline);
449+
450+
private static DateTime? IsTime(string text, Type targetType)
451+
{
452+
if (targetType != typeof(DateTime) && targetType != typeof(DateTime?))
453+
return null;
454+
455+
var match = reNow.Match(text);
456+
if (!match.Success)
457+
return null;
458+
459+
var amt = Int32.Parse(match.Groups[2].Value);
460+
if (match.Groups[1].Value == "-")
461+
amt = -amt;
462+
463+
switch (Char.ToUpperInvariant(match.Groups[3].Value[0]))
464+
{
465+
case 'S':
466+
return DateTime.Now.AddSeconds(amt);
467+
case 'M':
468+
return DateTime.Now.AddMinutes(amt);
469+
case 'H':
470+
return DateTime.Now.AddHours(amt);
471+
case 'D':
472+
return DateTime.Now.AddDays(amt);
473+
case 'Y':
474+
return DateTime.Now.AddYears(amt);
475+
}
476+
// it should not be possible to reach here.
477+
throw new ArgumentException();
478+
}
479+
434480
private static Type ElementType(Type seqType)
435481
{
436482
Type ienum = FindIEnumerable(seqType);
@@ -508,12 +554,14 @@ public static bool IsSimpleType(Type type)
508554
;
509555
}
510556
public static BindingFlags flags = BindingFlags.Instance | BindingFlags.Public;
511-
public static List<Member> GetFields(System.Type type, string memberName = null, string parentPath = null)
557+
public static List<Member> GetFields(Type type, string memberName = null, string parentPath = null)
512558
{
513559
List<Member> toReturn = new List<Member>();
514-
var fi = new Member();
515-
fi.Name = string.IsNullOrEmpty(parentPath) ? memberName : $"{parentPath}.{memberName}";
516-
fi.Type = type.ToString();
560+
var fi = new Member
561+
{
562+
Name = string.IsNullOrEmpty(parentPath) ? memberName : $"{parentPath}.{memberName}",
563+
Type = type.ToString()
564+
};
517565
fi.PossibleOperators = Member.Operators(type, string.IsNullOrEmpty(fi.Name));
518566
toReturn.Add(fi);
519567
if (!Member.IsSimpleType(type))
@@ -589,7 +637,7 @@ public static bool IsGenericList(Type type)
589637
mreOperator.IsDouble.ToString("g"),
590638
mreOperator.IsDecimal.ToString("g")
591639
};
592-
public static List<Operator> Operators(System.Type type, bool addLogicOperators = false, bool noOverloads = true)
640+
public static List<Operator> Operators(Type type, bool addLogicOperators = false, bool noOverloads = true)
593641
{
594642
List<Operator> operators = new List<Operator>();
595643
if (addLogicOperators)
@@ -788,10 +836,10 @@ public static DataRule Create(string member, mreOperator oper, object target, Ty
788836

789837
internal static class Placeholder
790838
{
791-
public static int Int;
792-
public static float Float;
793-
public static double Double;
794-
public static decimal Decimal;
839+
public static int Int = 0;
840+
public static float Float=0.0f;
841+
public static double Double=0.0;
842+
public static decimal Decimal=0.0m;
795843
}
796844

797845
// Nothing specific to MRE. Can be moved to a more common location
@@ -880,7 +928,7 @@ public enum mreOperator
880928
/// <summary>
881929
/// Checks that a string value matches a Regex expression
882930
/// </summary>
883-
IsMatch = 100,
931+
IsMatch = 100,
884932
/// <summary>
885933
/// Checks that a value can be 'TryParsed' to an Int32
886934
/// </summary>

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ You can reference member properties which are `Arrays` or `List<>` by their inde
5252
Rule rule = Rule.Create("Items[1].Cost", mreOperator.GreaterThanOrEqual, "5.25");
5353
```
5454

55+
Similarly, you can reference element of a string- or integer-keyed dictionary:
56+
```csharp
57+
Rule rule = Rule.Create("Items['myKey'].Cost", mreOperator.GreaterThanOrEqual, "5.25");
58+
```
59+
60+
5561
You can also compare an object to itself indicated by the `*.` at the beginning of the `TargetValue`:
5662
```csharp
5763
Rule rule = Rule.Create("Items[1].Cost", mreOperator.Equal, "*.Items[0].Cost");
@@ -92,8 +98,30 @@ There are a lot of examples in the test cases but, here is another snippet demon
9298

9399
If you need to run your comparison against an ADO.NET DataSet you can also do that as well:
94100
```csharp
95-
Rule rule = Rule.Create("Items[1].Cost", mreOperator.Equal, "*.Items[0].Cost");
101+
var dr = GetDataRow();
102+
// (int) dr["Column2"] == 123 && (string) dr["Column1"] == "Test"
103+
Rule rule = DataRule.Create<int>("Column2", mreOperator.Equal, "123") & DataRule.Create<string>("Column1", mreOperator.Equal, "Test");
96104
```
105+
106+
107+
108+
#### #NOW and time-based rules.
109+
You can test a property for a time range from the current time, using the special case `#NOW` keyword. The member must be a `DataTime` or `DateTime?`,
110+
and the target value must be a string in the form :`#NOW+90D` (The sign can be plus or minus, but must be given. The Suffix can be
111+
'S' for Seconds, `M` for Minutes, `H` for Hours, `D` for Days, or `Y` for Years. The number must be an integer.)
112+
113+
examples:
114+
115+
` Rule rule = Rule.Create("OrderDate", mreOperator.GreaterThanOrEqual, "#NOW-90M");`
116+
117+
`OrderDate` must be within the last 90 minutes.
118+
119+
` Rule rule = Rule.Create("ExpirationDate", mreOperator.LessThanOrEqual, "#NOW+1Y");`
120+
121+
`ExpirationDate` must be within the next year.
122+
123+
124+
97125

98126
How Can I Store Rules?
99127
---------------------

0 commit comments

Comments
 (0)