LotSizingDataModel.Instance 2.0.1
Lot-sizing instance representation, descriptors and problem characterization.
Loading...
Searching...
No Matches
KnownProblemRuleEvaluator.cs
Go to the documentation of this file.
1using System;
2using System.Collections;
3using System.Collections.Generic;
4using System.ComponentModel;
5using System.Globalization;
6using System.Linq;
7using System.Reflection;
8
10
11/// <summary>
12/// Evaluates known-problem classification rules against
13/// extracted lot-sizing problem features.
14/// </summary>
15/// <remarks>
16/// The evaluator reads feature properties dynamically from
17/// <see cref="LotSizingProblemFeatures"/>.
18///
19/// Feature lookup is case-insensitive. Consequently,
20/// <c>itemCount</c>, <c>ItemCount</c> and <c>ITEMCOUNT</c>
21/// identify the same feature.
22/// </remarks>
23public static class KnownProblemRuleEvaluator
24{
25 private static readonly IReadOnlyDictionary<
26 string,
27 PropertyInfo> FeatureProperties =
28 BuildFeaturePropertyIndex();
29
30 /// <summary>
31 /// Evaluates one classification rule against a
32 /// lot-sizing problem-feature profile.
33 /// </summary>
34 /// <param name="rule">
35 /// Rule definition to evaluate.
36 /// </param>
37 /// <param name="features">
38 /// Extracted lot-sizing problem features.
39 /// </param>
40 /// <param name="isRequired">
41 /// Value indicating whether this rule is mandatory for
42 /// the problem-family match currently being evaluated.
43 /// </param>
44 /// <returns>
45 /// Evidence containing the expected value, observed value
46 /// and evaluation result.
47 /// </returns>
48 /// <exception cref="ArgumentNullException">
49 /// Thrown when <paramref name="rule"/> or
50 /// <paramref name="features"/> is
51 /// <see langword="null"/>.
52 /// </exception>
53 /// <exception cref="InvalidOperationException">
54 /// Thrown when the rule is disabled, invalid, references
55 /// an unknown feature or cannot be evaluated using the
56 /// selected operator.
57 /// </exception>
61 bool isRequired = true)
62 {
63 ArgumentNullException.ThrowIfNull(rule);
64 ArgumentNullException.ThrowIfNull(features);
65
66 EnsureRuleCanBeEvaluated(rule);
67
68 PropertyInfo featureProperty =
69 GetFeatureProperty(rule.FeatureCode);
70
71 object? observedValue;
72
73 try
74 {
75 observedValue =
76 featureProperty.GetValue(features);
77 }
78 catch (TargetInvocationException exception)
79 {
80 throw new InvalidOperationException(
81 $"Feature '{rule.FeatureCode}' could not " +
82 "be read.",
83 exception.InnerException ?? exception);
84 }
85 catch (Exception exception)
86 when (exception is MethodAccessException or
87 TargetException)
88 {
89 throw new InvalidOperationException(
90 $"Feature '{rule.FeatureCode}' could not " +
91 "be read.",
92 exception);
93 }
94
95 bool isSatisfied =
96 EvaluateCondition(
97 rule,
98 observedValue,
99 featureProperty.PropertyType);
100
101 var evidence =
103 featureCode:
104 featureProperty.Name,
105
106 expectedValue:
107 GetDisplayedExpectedValue(rule),
108
109 observedValue:
110 FormatValue(observedValue),
111
112 isSatisfied:
113 isSatisfied,
114
115 isRequired:
116 isRequired,
117
118 description:
119 rule.Description)
120 {
121 RuleCode =
122 rule.RuleCode,
123
124 Weight =
125 rule.Weight
126 };
127
128 return evidence;
129 }
130
131 /// <summary>
132 /// Attempts to evaluate one classification rule without
133 /// propagating configuration or conversion exceptions.
134 /// </summary>
135 /// <param name="rule">
136 /// Rule definition to evaluate.
137 /// </param>
138 /// <param name="features">
139 /// Extracted lot-sizing problem features.
140 /// </param>
141 /// <param name="isRequired">
142 /// Value indicating whether this rule is mandatory for
143 /// the current problem-family match.
144 /// </param>
145 /// <param name="evidence">
146 /// Evidence produced when evaluation succeeds.
147 /// </param>
148 /// <param name="errorMessage">
149 /// Error message produced when evaluation fails.
150 /// </param>
151 /// <returns>
152 /// <see langword="true"/> when the rule was evaluated;
153 /// otherwise, <see langword="false"/>.
154 /// </returns>
155 public static bool TryEvaluate(
158 bool isRequired,
159 out ClassificationEvidence? evidence,
160 out string errorMessage)
161 {
162 try
163 {
164 evidence =
165 Evaluate(
166 rule,
167 features,
168 isRequired);
169
170 errorMessage =
171 string.Empty;
172
173 return true;
174 }
175 catch (Exception exception)
176 when (exception is ArgumentException or
177 InvalidOperationException or
178 NotSupportedException or
179 FormatException or
180 OverflowException)
181 {
182 evidence =
183 null;
184
185 errorMessage =
186 exception.Message;
187
188 return false;
189 }
190 }
191
192 /// <summary>
193 /// Determines whether a feature code identifies a
194 /// readable property of
195 /// <see cref="LotSizingProblemFeatures"/>.
196 /// </summary>
197 /// <param name="featureCode">
198 /// Feature code to examine.
199 /// </param>
200 /// <returns>
201 /// <see langword="true"/> when the feature exists;
202 /// otherwise, <see langword="false"/>.
203 /// </returns>
204 public static bool IsKnownFeatureCode(
205 string featureCode)
206 {
207 if (string.IsNullOrWhiteSpace(featureCode))
208 {
209 return false;
210 }
211
212 return FeatureProperties.ContainsKey(
213 featureCode.Trim());
214 }
215
216 /// <summary>
217 /// Returns all feature codes supported by the standard
218 /// rule evaluator.
219 /// </summary>
220 /// <returns>
221 /// Ordered collection of readable feature-property names.
222 /// </returns>
223 public static IReadOnlyList<string>
225 {
226 return FeatureProperties
227 .Values
228 .Select(
229 property =>
230 property.Name)
231 .Distinct(
232 StringComparer.OrdinalIgnoreCase)
233 .OrderBy(
234 propertyName =>
235 propertyName,
236 StringComparer.OrdinalIgnoreCase)
237 .ToArray();
238 }
239
240 private static void EnsureRuleCanBeEvaluated(
242 {
243 if (!rule.IsEnabled)
244 {
245 throw new InvalidOperationException(
246 $"Classification rule '{DisplayRuleCode(rule)}' " +
247 "is disabled.");
248 }
249
250 if (!rule.IsValidDefinition)
251 {
252 throw new InvalidOperationException(
253 $"Classification rule '{DisplayRuleCode(rule)}' " +
254 "is incomplete or invalid.");
255 }
256
258 {
259 throw new InvalidOperationException(
260 $"Classification rule '{DisplayRuleCode(rule)}' " +
261 $"references unknown feature " +
262 $"'{rule.FeatureCode}'.");
263 }
264 }
265
266 private static PropertyInfo GetFeatureProperty(
267 string featureCode)
268 {
269 string normalizedFeatureCode =
270 featureCode.Trim();
271
272 if (FeatureProperties.TryGetValue(
273 normalizedFeatureCode,
274 out PropertyInfo? property))
275 {
276 return property;
277 }
278
279 throw new InvalidOperationException(
280 $"Unknown lot-sizing problem feature " +
281 $"'{featureCode}'.");
282 }
283
284 private static bool EvaluateCondition(
285 KnownProblemRuleDefinition rule,
286 object? observedValue,
287 Type featureType)
288 {
289 string operatorCode =
290 rule.OperatorCode;
291
292 return operatorCode switch
293 {
294 KnownProblemRuleDefinition.IsTrueOperator =>
295 EvaluateBooleanCondition(
296 observedValue,
297 expectedValue: true),
298
299 KnownProblemRuleDefinition.IsFalseOperator =>
300 EvaluateBooleanCondition(
301 observedValue,
302 expectedValue: false),
303
304 KnownProblemRuleDefinition.EqualsOperator =>
305 AreEqual(
306 observedValue,
307 featureType,
308 rule.ExpectedValue),
309
310 KnownProblemRuleDefinition.NotEqualsOperator =>
311 !AreEqual(
312 observedValue,
313 featureType,
314 rule.ExpectedValue),
315
316 KnownProblemRuleDefinition.GreaterThanOperator =>
317 Compare(
318 observedValue,
319 featureType,
320 rule.ExpectedValue) > 0,
321
322 KnownProblemRuleDefinition
323 .GreaterThanOrEqualOperator =>
324 Compare(
325 observedValue,
326 featureType,
327 rule.ExpectedValue) >= 0,
328
329 KnownProblemRuleDefinition.LessThanOperator =>
330 Compare(
331 observedValue,
332 featureType,
333 rule.ExpectedValue) < 0,
334
335 KnownProblemRuleDefinition
336 .LessThanOrEqualOperator =>
337 Compare(
338 observedValue,
339 featureType,
340 rule.ExpectedValue) <= 0,
341
342 KnownProblemRuleDefinition.ContainsOperator =>
343 Contains(
344 observedValue,
345 featureType,
346 rule.ExpectedValue),
347
348 KnownProblemRuleDefinition.NotContainsOperator =>
349 !Contains(
350 observedValue,
351 featureType,
352 rule.ExpectedValue),
353
354 KnownProblemRuleDefinition.InOperator =>
355 IsInExpectedValues(
356 observedValue,
357 featureType,
358 rule.ExpectedValue),
359
360 KnownProblemRuleDefinition.NotInOperator =>
361 !IsInExpectedValues(
362 observedValue,
363 featureType,
364 rule.ExpectedValue),
365
366 _ =>
367 throw new NotSupportedException(
368 $"Operator '{operatorCode}' is not " +
369 "supported by the standard evaluator.")
370 };
371 }
372
373 private static bool EvaluateBooleanCondition(
374 object? observedValue,
375 bool expectedValue)
376 {
377 if (observedValue is not bool observedBoolean)
378 {
379 throw new InvalidOperationException(
380 "The IS_TRUE and IS_FALSE operators can only " +
381 "be applied to Boolean features.");
382 }
383
384 return observedBoolean ==
385 expectedValue;
386 }
387
388 private static bool AreEqual(
389 object? observedValue,
390 Type featureType,
391 string expectedText)
392 {
393 if (observedValue is null)
394 {
395 return IsNullLiteral(expectedText);
396 }
397
398 if (IsNullLiteral(expectedText))
399 {
400 return false;
401 }
402
403 Type effectiveType =
404 GetEffectiveType(featureType);
405
406 object expectedValue =
407 ConvertTextToType(
408 expectedText,
409 effectiveType);
410
411 if (effectiveType == typeof(string))
412 {
413 return string.Equals(
414 Convert.ToString(
415 observedValue,
416 CultureInfo.InvariantCulture),
417 Convert.ToString(
418 expectedValue,
419 CultureInfo.InvariantCulture),
420 StringComparison.OrdinalIgnoreCase);
421 }
422
423 if (IsNumericType(effectiveType))
424 {
425 double observedNumber =
426 Convert.ToDouble(
427 observedValue,
428 CultureInfo.InvariantCulture);
429
430 double expectedNumber =
431 Convert.ToDouble(
432 expectedValue,
433 CultureInfo.InvariantCulture);
434
435 return observedNumber.Equals(
436 expectedNumber);
437 }
438
439 return Equals(
440 observedValue,
441 expectedValue);
442 }
443
444 private static int Compare(
445 object? observedValue,
446 Type featureType,
447 string expectedText)
448 {
449 if (observedValue is null)
450 {
451 throw new InvalidOperationException(
452 "A null feature value cannot be used with an " +
453 "ordered comparison operator.");
454 }
455
456 Type effectiveType =
457 GetEffectiveType(featureType);
458
459 object expectedValue =
460 ConvertTextToType(
461 expectedText,
462 effectiveType);
463
464 if (IsNumericType(effectiveType))
465 {
466 double observedNumber =
467 Convert.ToDouble(
468 observedValue,
469 CultureInfo.InvariantCulture);
470
471 double expectedNumber =
472 Convert.ToDouble(
473 expectedValue,
474 CultureInfo.InvariantCulture);
475
476 return observedNumber.CompareTo(
477 expectedNumber);
478 }
479
480 if (effectiveType == typeof(string))
481 {
482 return string.Compare(
483 Convert.ToString(
484 observedValue,
485 CultureInfo.InvariantCulture),
486 Convert.ToString(
487 expectedValue,
488 CultureInfo.InvariantCulture),
489 StringComparison.OrdinalIgnoreCase);
490 }
491
492 if (observedValue is IComparable comparable)
493 {
494 return comparable.CompareTo(
495 expectedValue);
496 }
497
498 throw new InvalidOperationException(
499 $"Feature type '{effectiveType.FullName}' does " +
500 "not support ordered comparisons.");
501 }
502
503 private static bool Contains(
504 object? observedValue,
505 Type featureType,
506 string expectedText)
507 {
508 if (observedValue is null)
509 {
510 return false;
511 }
512
513 if (observedValue is string observedText)
514 {
515 return observedText.IndexOf(
516 expectedText,
517 StringComparison.OrdinalIgnoreCase) >= 0;
518 }
519
520 if (observedValue is not IEnumerable enumerable)
521 {
522 throw new InvalidOperationException(
523 "The CONTAINS operator can only be applied " +
524 "to text or collection features.");
525 }
526
527 Type? elementType =
528 TryGetEnumerableElementType(
529 featureType);
530
531 foreach (object? element
532 in enumerable)
533 {
534 if (element is null)
535 {
536 if (IsNullLiteral(expectedText))
537 {
538 return true;
539 }
540
541 continue;
542 }
543
544 Type comparisonType =
545 elementType ??
546 element.GetType();
547
548 if (AreEqual(
549 element,
550 comparisonType,
551 expectedText))
552 {
553 return true;
554 }
555 }
556
557 return false;
558 }
559
560 private static bool IsInExpectedValues(
561 object? observedValue,
562 Type featureType,
563 string expectedText)
564 {
565 string[] expectedValues =
566 SplitExpectedValues(
567 expectedText);
568
569 if (expectedValues.Length == 0)
570 {
571 throw new InvalidOperationException(
572 "The IN and NOT_IN operators require at " +
573 "least one expected value.");
574 }
575
576 return expectedValues.Any(
577 value =>
578 AreEqual(
579 observedValue,
580 featureType,
581 value));
582 }
583
584 private static object ConvertTextToType(
585 string text,
586 Type targetType)
587 {
588 Type effectiveType =
589 GetEffectiveType(targetType);
590
591 string normalizedText =
592 text.Trim();
593
594 if (effectiveType == typeof(string))
595 {
596 return normalizedText;
597 }
598
599 if (effectiveType == typeof(bool))
600 {
601 return ParseBoolean(
602 normalizedText);
603 }
604
605 if (effectiveType.IsEnum)
606 {
607 try
608 {
609 return Enum.Parse(
610 effectiveType,
611 normalizedText,
612 ignoreCase: true);
613 }
614 catch (ArgumentException exception)
615 {
616 string allowedValues =
617 string.Join(
618 ", ",
619 Enum.GetNames(effectiveType));
620
621 throw new FormatException(
622 $"Value '{text}' is not valid for " +
623 $"enumeration '{effectiveType.Name}'. " +
624 $"Allowed values are: {allowedValues}.",
625 exception);
626 }
627 }
628
629 if (effectiveType == typeof(Guid))
630 {
631 return Guid.Parse(
632 normalizedText);
633 }
634
635 if (effectiveType == typeof(DateTime))
636 {
637 return DateTime.Parse(
638 normalizedText,
639 CultureInfo.InvariantCulture,
640 DateTimeStyles.RoundtripKind);
641 }
642
643 if (effectiveType == typeof(DateTimeOffset))
644 {
645 return DateTimeOffset.Parse(
646 normalizedText,
647 CultureInfo.InvariantCulture,
648 DateTimeStyles.RoundtripKind);
649 }
650
651 if (effectiveType == typeof(TimeSpan))
652 {
653 return TimeSpan.Parse(
654 normalizedText,
655 CultureInfo.InvariantCulture);
656 }
657
658 TypeConverter converter =
659 TypeDescriptor.GetConverter(
660 effectiveType);
661
662 if (converter.CanConvertFrom(
663 typeof(string)))
664 {
665 object? convertedValue =
666 converter.ConvertFrom(
667 context: null,
668 culture:
669 CultureInfo.InvariantCulture,
670 value:
671 normalizedText);
672
673 if (convertedValue is not null)
674 {
675 return convertedValue;
676 }
677 }
678
679 try
680 {
681 return Convert.ChangeType(
682 normalizedText,
683 effectiveType,
684 CultureInfo.InvariantCulture);
685 }
686 catch (Exception exception)
687 when (exception is InvalidCastException or
688 FormatException or
689 OverflowException)
690 {
691 throw new FormatException(
692 $"Value '{text}' cannot be converted to " +
693 $"feature type '{effectiveType.Name}'.",
694 exception);
695 }
696 }
697
698 private static bool ParseBoolean(
699 string text)
700 {
701 if (bool.TryParse(
702 text,
703 out bool booleanValue))
704 {
705 return booleanValue;
706 }
707
708 return text.Trim().ToUpperInvariant() switch
709 {
710 "1" or "YES" or "Y" =>
711 true,
712
713 "0" or "NO" or "N" =>
714 false,
715
716 _ =>
717 throw new FormatException(
718 $"Value '{text}' is not a valid Boolean " +
719 "value.")
720 };
721 }
722
723 private static string[] SplitExpectedValues(
724 string expectedText)
725 {
726 return expectedText
727 .Split(
728 ';',
729 StringSplitOptions.RemoveEmptyEntries |
730 StringSplitOptions.TrimEntries)
731 .Distinct(
732 StringComparer.OrdinalIgnoreCase)
733 .ToArray();
734 }
735
736 private static string GetDisplayedExpectedValue(
737 KnownProblemRuleDefinition rule)
738 {
739 return rule.OperatorCode switch
740 {
741 KnownProblemRuleDefinition.IsTrueOperator =>
742 "true",
743
744 KnownProblemRuleDefinition.IsFalseOperator =>
745 "false",
746
747 _ =>
748 rule.ExpectedValue
749 };
750 }
751
752 private static string FormatValue(
753 object? value)
754 {
755 if (value is null)
756 {
757 return "<null>";
758 }
759
760 if (value is bool booleanValue)
761 {
762 return booleanValue
763 ? "true"
764 : "false";
765 }
766
767 if (value is DateTime dateTime)
768 {
769 return dateTime.ToString(
770 "O",
771 CultureInfo.InvariantCulture);
772 }
773
774 if (value is DateTimeOffset dateTimeOffset)
775 {
776 return dateTimeOffset.ToString(
777 "O",
778 CultureInfo.InvariantCulture);
779 }
780
781 if (value is IEnumerable enumerable &&
782 value is not string)
783 {
784 var formattedElements =
785 new List<string>();
786
787 foreach (object? element
788 in enumerable)
789 {
790 formattedElements.Add(
791 FormatValue(element));
792 }
793
794 return string.Join(
795 ";",
796 formattedElements);
797 }
798
799 if (value is IFormattable formattable)
800 {
801 return formattable.ToString(
802 format: null,
803 formatProvider:
804 CultureInfo.InvariantCulture) ??
805 string.Empty;
806 }
807
808 return value.ToString() ??
809 string.Empty;
810 }
811
812 private static Type GetEffectiveType(
813 Type type)
814 {
815 return Nullable.GetUnderlyingType(type) ??
816 type;
817 }
818
819 private static bool IsNumericType(
820 Type type)
821 {
822 Type effectiveType =
823 GetEffectiveType(type);
824
825 return Type.GetTypeCode(effectiveType) switch
826 {
827 TypeCode.Byte or
828 TypeCode.SByte or
829 TypeCode.UInt16 or
830 TypeCode.UInt32 or
831 TypeCode.UInt64 or
832 TypeCode.Int16 or
833 TypeCode.Int32 or
834 TypeCode.Int64 or
835 TypeCode.Decimal or
836 TypeCode.Double or
837 TypeCode.Single =>
838 true,
839
840 _ =>
841 false
842 };
843 }
844
845 private static bool IsNullLiteral(
846 string text)
847 {
848 return string.Equals(
849 text.Trim(),
850 "NULL",
851 StringComparison.OrdinalIgnoreCase) ||
852 string.Equals(
853 text.Trim(),
854 "<NULL>",
855 StringComparison.OrdinalIgnoreCase);
856 }
857
858 private static Type? TryGetEnumerableElementType(
859 Type collectionType)
860 {
861 if (collectionType.IsArray)
862 {
863 return collectionType.GetElementType();
864 }
865
866 if (collectionType.IsGenericType &&
867 collectionType.GetGenericTypeDefinition() ==
868 typeof(IEnumerable<>))
869 {
870 return collectionType
871 .GetGenericArguments()[0];
872 }
873
874 Type? enumerableInterface =
875 collectionType
876 .GetInterfaces()
877 .FirstOrDefault(
878 implementedInterface =>
879 implementedInterface.IsGenericType &&
880 implementedInterface
881 .GetGenericTypeDefinition() ==
882 typeof(IEnumerable<>));
883
884 return enumerableInterface?
885 .GetGenericArguments()[0];
886 }
887
888 private static IReadOnlyDictionary<
889 string,
890 PropertyInfo> BuildFeaturePropertyIndex()
891 {
892 return typeof(LotSizingProblemFeatures)
893 .GetProperties(
894 BindingFlags.Instance |
895 BindingFlags.Public)
896 .Where(
897 property =>
898 property.CanRead &&
899 property.GetIndexParameters().Length == 0)
900 .ToDictionary(
901 property =>
902 property.Name,
903 property =>
904 property,
905 StringComparer.OrdinalIgnoreCase);
906 }
907
908 private static string DisplayRuleCode(
909 KnownProblemRuleDefinition rule)
910 {
911 return string.IsNullOrWhiteSpace(
912 rule.RuleCode)
913 ? "<missing code>"
914 : rule.RuleCode;
915 }
916}
Represents one factual element used to evaluate whether a lot-sizing instance matches a known problem...
Defines one reusable condition used to classify lot-sizing problem instances.
string FeatureCode
Gets or sets the stable code of the feature evaluated by this rule.
string RuleCode
Gets or sets the stable code identifying this classification rule.
string Description
Gets or sets a human-readable description of the classification rule.
bool IsEnabled
Gets or sets a value indicating whether this rule is enabled for automatic evaluation.
bool IsValidDefinition
Gets a value indicating whether this rule contains the metadata required for evaluation.
Evaluates known-problem classification rules against extracted lot-sizing problem features.
static IReadOnlyList< string > GetKnownFeatureCodes()
Returns all feature codes supported by the standard rule evaluator.
static bool IsKnownFeatureCode(string featureCode)
Determines whether a feature code identifies a readable property of LotSizingProblemFeatures.
static bool TryEvaluate(KnownProblemRuleDefinition rule, LotSizingProblemFeatures features, bool isRequired, out ClassificationEvidence? evidence, out string errorMessage)
Attempts to evaluate one classification rule without propagating configuration or conversion exceptio...
static ClassificationEvidence Evaluate(KnownProblemRuleDefinition rule, LotSizingProblemFeatures features, bool isRequired=true)
Evaluates one classification rule against a lot-sizing problem-feature profile.
Describes the factual structural and modeling features detected in a lot-sizing supply-chain instance...