25 private static readonly IReadOnlyDictionary<
27 PropertyInfo> FeatureProperties =
28 BuildFeaturePropertyIndex();
61 bool isRequired =
true)
63 ArgumentNullException.ThrowIfNull(rule);
64 ArgumentNullException.ThrowIfNull(features);
66 EnsureRuleCanBeEvaluated(rule);
68 PropertyInfo featureProperty =
71 object? observedValue;
76 featureProperty.GetValue(features);
78 catch (TargetInvocationException exception)
80 throw new InvalidOperationException(
81 $
"Feature '{rule.FeatureCode}' could not " +
83 exception.InnerException ?? exception);
85 catch (Exception exception)
86 when (exception is MethodAccessException or
89 throw new InvalidOperationException(
90 $
"Feature '{rule.FeatureCode}' could not " +
99 featureProperty.PropertyType);
104 featureProperty.Name,
107 GetDisplayedExpectedValue(rule),
110 FormatValue(observedValue),
160 out
string errorMessage)
175 catch (Exception exception)
176 when (exception is ArgumentException or
177 InvalidOperationException or
178 NotSupportedException or
207 if (
string.IsNullOrWhiteSpace(featureCode))
212 return FeatureProperties.ContainsKey(
223 public static IReadOnlyList<string>
226 return FeatureProperties
232 StringComparer.OrdinalIgnoreCase)
236 StringComparer.OrdinalIgnoreCase)
240 private static void EnsureRuleCanBeEvaluated(
245 throw new InvalidOperationException(
246 $
"Classification rule '{DisplayRuleCode(rule)}' " +
252 throw new InvalidOperationException(
253 $
"Classification rule '{DisplayRuleCode(rule)}' " +
254 "is incomplete or invalid.");
259 throw new InvalidOperationException(
260 $
"Classification rule '{DisplayRuleCode(rule)}' " +
261 $
"references unknown feature " +
262 $
"'{rule.FeatureCode}'.");
266 private static PropertyInfo GetFeatureProperty(
269 string normalizedFeatureCode =
272 if (FeatureProperties.TryGetValue(
273 normalizedFeatureCode,
274 out PropertyInfo? property))
279 throw new InvalidOperationException(
280 $
"Unknown lot-sizing problem feature " +
281 $
"'{featureCode}'.");
284 private static bool EvaluateCondition(
285 KnownProblemRuleDefinition rule,
286 object? observedValue,
289 string operatorCode =
292 return operatorCode
switch
294 KnownProblemRuleDefinition.IsTrueOperator =>
295 EvaluateBooleanCondition(
297 expectedValue:
true),
299 KnownProblemRuleDefinition.IsFalseOperator =>
300 EvaluateBooleanCondition(
302 expectedValue:
false),
304 KnownProblemRuleDefinition.EqualsOperator =>
310 KnownProblemRuleDefinition.NotEqualsOperator =>
316 KnownProblemRuleDefinition.GreaterThanOperator =>
320 rule.ExpectedValue) > 0,
322 KnownProblemRuleDefinition
323 .GreaterThanOrEqualOperator =>
327 rule.ExpectedValue) >= 0,
329 KnownProblemRuleDefinition.LessThanOperator =>
333 rule.ExpectedValue) < 0,
335 KnownProblemRuleDefinition
336 .LessThanOrEqualOperator =>
340 rule.ExpectedValue) <= 0,
342 KnownProblemRuleDefinition.ContainsOperator =>
348 KnownProblemRuleDefinition.NotContainsOperator =>
354 KnownProblemRuleDefinition.InOperator =>
360 KnownProblemRuleDefinition.NotInOperator =>
367 throw new NotSupportedException(
368 $
"Operator '{operatorCode}' is not " +
369 "supported by the standard evaluator.")
373 private static bool EvaluateBooleanCondition(
374 object? observedValue,
377 if (observedValue is not
bool observedBoolean)
379 throw new InvalidOperationException(
380 "The IS_TRUE and IS_FALSE operators can only " +
381 "be applied to Boolean features.");
384 return observedBoolean ==
388 private static bool AreEqual(
389 object? observedValue,
393 if (observedValue is
null)
395 return IsNullLiteral(expectedText);
398 if (IsNullLiteral(expectedText))
404 GetEffectiveType(featureType);
406 object expectedValue =
411 if (effectiveType == typeof(
string))
413 return string.Equals(
416 CultureInfo.InvariantCulture),
419 CultureInfo.InvariantCulture),
420 StringComparison.OrdinalIgnoreCase);
423 if (IsNumericType(effectiveType))
425 double observedNumber =
428 CultureInfo.InvariantCulture);
430 double expectedNumber =
433 CultureInfo.InvariantCulture);
435 return observedNumber.Equals(
444 private static int Compare(
445 object? observedValue,
449 if (observedValue is
null)
451 throw new InvalidOperationException(
452 "A null feature value cannot be used with an " +
453 "ordered comparison operator.");
457 GetEffectiveType(featureType);
459 object expectedValue =
464 if (IsNumericType(effectiveType))
466 double observedNumber =
469 CultureInfo.InvariantCulture);
471 double expectedNumber =
474 CultureInfo.InvariantCulture);
476 return observedNumber.CompareTo(
480 if (effectiveType == typeof(
string))
482 return string.Compare(
485 CultureInfo.InvariantCulture),
488 CultureInfo.InvariantCulture),
489 StringComparison.OrdinalIgnoreCase);
492 if (observedValue is IComparable comparable)
494 return comparable.CompareTo(
498 throw new InvalidOperationException(
499 $
"Feature type '{effectiveType.FullName}' does " +
500 "not support ordered comparisons.");
503 private static bool Contains(
504 object? observedValue,
508 if (observedValue is
null)
513 if (observedValue is
string observedText)
515 return observedText.IndexOf(
517 StringComparison.OrdinalIgnoreCase) >= 0;
520 if (observedValue is not IEnumerable enumerable)
522 throw new InvalidOperationException(
523 "The CONTAINS operator can only be applied " +
524 "to text or collection features.");
528 TryGetEnumerableElementType(
531 foreach (
object? element
536 if (IsNullLiteral(expectedText))
544 Type comparisonType =
560 private static bool IsInExpectedValues(
561 object? observedValue,
565 string[] expectedValues =
569 if (expectedValues.Length == 0)
571 throw new InvalidOperationException(
572 "The IN and NOT_IN operators require at " +
573 "least one expected value.");
576 return expectedValues.Any(
584 private static object ConvertTextToType(
589 GetEffectiveType(targetType);
591 string normalizedText =
594 if (effectiveType == typeof(
string))
596 return normalizedText;
599 if (effectiveType == typeof(
bool))
605 if (effectiveType.IsEnum)
614 catch (ArgumentException exception)
616 string allowedValues =
619 Enum.GetNames(effectiveType));
621 throw new FormatException(
622 $
"Value '{text}' is not valid for " +
623 $
"enumeration '{effectiveType.Name}'. " +
624 $
"Allowed values are: {allowedValues}.",
629 if (effectiveType == typeof(Guid))
635 if (effectiveType == typeof(DateTime))
637 return DateTime.Parse(
639 CultureInfo.InvariantCulture,
640 DateTimeStyles.RoundtripKind);
643 if (effectiveType == typeof(DateTimeOffset))
645 return DateTimeOffset.Parse(
647 CultureInfo.InvariantCulture,
648 DateTimeStyles.RoundtripKind);
651 if (effectiveType == typeof(TimeSpan))
653 return TimeSpan.Parse(
655 CultureInfo.InvariantCulture);
658 TypeConverter converter =
659 TypeDescriptor.GetConverter(
662 if (converter.CanConvertFrom(
665 object? convertedValue =
666 converter.ConvertFrom(
669 CultureInfo.InvariantCulture,
673 if (convertedValue is not
null)
675 return convertedValue;
681 return Convert.ChangeType(
684 CultureInfo.InvariantCulture);
686 catch (Exception exception)
687 when (exception is InvalidCastException or
691 throw new FormatException(
692 $
"Value '{text}' cannot be converted to " +
693 $
"feature type '{effectiveType.Name}'.",
698 private static bool ParseBoolean(
703 out
bool booleanValue))
708 return text.Trim().ToUpperInvariant()
switch
710 "1" or
"YES" or
"Y" =>
713 "0" or
"NO" or
"N" =>
717 throw new FormatException(
718 $
"Value '{text}' is not a valid Boolean " +
723 private static string[] SplitExpectedValues(
729 StringSplitOptions.RemoveEmptyEntries |
730 StringSplitOptions.TrimEntries)
732 StringComparer.OrdinalIgnoreCase)
736 private static string GetDisplayedExpectedValue(
737 KnownProblemRuleDefinition rule)
739 return rule.OperatorCode
switch
741 KnownProblemRuleDefinition.IsTrueOperator =>
744 KnownProblemRuleDefinition.IsFalseOperator =>
752 private static string FormatValue(
760 if (value is
bool booleanValue)
767 if (value is DateTime dateTime)
769 return dateTime.ToString(
771 CultureInfo.InvariantCulture);
774 if (value is DateTimeOffset dateTimeOffset)
776 return dateTimeOffset.ToString(
778 CultureInfo.InvariantCulture);
781 if (value is IEnumerable enumerable &&
784 var formattedElements =
787 foreach (
object? element
790 formattedElements.Add(
791 FormatValue(element));
799 if (value is IFormattable formattable)
801 return formattable.ToString(
804 CultureInfo.InvariantCulture) ??
808 return value.ToString() ??
812 private static Type GetEffectiveType(
815 return Nullable.GetUnderlyingType(type) ??
819 private static bool IsNumericType(
823 GetEffectiveType(type);
825 return Type.GetTypeCode(effectiveType)
switch
845 private static bool IsNullLiteral(
848 return string.Equals(
851 StringComparison.OrdinalIgnoreCase) ||
855 StringComparison.OrdinalIgnoreCase);
858 private static Type? TryGetEnumerableElementType(
861 if (collectionType.IsArray)
863 return collectionType.GetElementType();
866 if (collectionType.IsGenericType &&
867 collectionType.GetGenericTypeDefinition() ==
868 typeof(IEnumerable<>))
870 return collectionType
871 .GetGenericArguments()[0];
874 Type? enumerableInterface =
878 implementedInterface =>
879 implementedInterface.IsGenericType &&
881 .GetGenericTypeDefinition() ==
882 typeof(IEnumerable<>));
884 return enumerableInterface?
885 .GetGenericArguments()[0];
888 private static IReadOnlyDictionary<
890 PropertyInfo> BuildFeaturePropertyIndex()
892 return typeof(LotSizingProblemFeatures)
894 BindingFlags.Instance |
899 property.GetIndexParameters().Length == 0)
905 StringComparer.OrdinalIgnoreCase);
908 private static string DisplayRuleCode(
909 KnownProblemRuleDefinition rule)
911 return string.IsNullOrWhiteSpace(