ComparisonFunctions.java

/*
 * SPDX-FileCopyrightText: none
 * SPDX-License-Identifier: CC0-1.0
 */

package gov.nist.secauto.metaschema.core.metapath.function;

import gov.nist.secauto.metaschema.core.metapath.ISequence;
import gov.nist.secauto.metaschema.core.metapath.InvalidTypeMetapathException;
import gov.nist.secauto.metaschema.core.metapath.function.library.FnNot;
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IBase64BinaryItem;
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IBooleanItem;
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IDateItem;
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IDateTimeItem;
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IDayTimeDurationItem;
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IDecimalItem;
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IDurationItem;
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IIntegerItem;
import gov.nist.secauto.metaschema.core.metapath.item.atomic.INumericItem;
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IStringItem;
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IUntypedAtomicItem;
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IYearMonthDurationItem;

import java.util.Locale;

import edu.umd.cs.findbugs.annotations.NonNull;

@SuppressWarnings({ "PMD.GodClass", "PMD.CyclomaticComplexity" })
public final class ComparisonFunctions {
  /**
   * Comparison operators.
   */
  public enum Operator {
    /**
     * An equal comparison.
     */
    EQ,
    /**
     * A not equal comparison.
     */
    NE,
    /**
     * A less than comparison.
     */
    LT,
    /**
     * A less than or equal comparison.
     */
    LE,
    /**
     * A greater than comparison.
     */
    GT,
    /**
     * A greater than or equal comparison.
     */
    GE;
  }

  private ComparisonFunctions() {
    // disable construction
  }

  /**
   * Compare the two items using the provided {@code operator}.
   *
   * @param leftItem
   *          the first item to compare
   * @param operator
   *          the comparison operator
   * @param rightItem
   *          the second item to compare
   * @return the result of the comparison
   */
  @NonNull
  public static IBooleanItem valueCompairison(@NonNull IAnyAtomicItem leftItem, @NonNull Operator operator,
      @NonNull IAnyAtomicItem rightItem) {
    return compare(leftItem, operator, rightItem);
  }

  /**
   * Compare the sets of atomic items.
   *
   * @param leftItems
   *          the first set of items to compare
   * @param operator
   *          the comparison operator
   * @param rightItems
   *          the second set of items to compare
   * @return a or an empty {@link ISequence} if either item is {@code null}
   */
  @NonNull
  public static IBooleanItem generalCompairison( // NOPMD - acceptable complexity
      @NonNull ISequence<? extends IAnyAtomicItem> leftItems,
      @NonNull Operator operator,
      @NonNull ISequence<? extends IAnyAtomicItem> rightItems) {

    IBooleanItem retval = IBooleanItem.FALSE;
    for (IAnyAtomicItem left : leftItems.getValue()) {
      assert left != null;
      for (IAnyAtomicItem right : rightItems.getValue()) {
        assert right != null;
        IAnyAtomicItem leftCast;
        IAnyAtomicItem rightCast;
        if (left instanceof IUntypedAtomicItem) {
          if (right instanceof IUntypedAtomicItem) {
            leftCast = IStringItem.cast(left);
            rightCast = IStringItem.cast(right);
          } else {
            leftCast = applyGeneralComparisonCast(right, left);
            rightCast = right;
          }
        } else if (right instanceof IUntypedAtomicItem) {
          leftCast = left;
          rightCast = applyGeneralComparisonCast(left, right);
        } else {
          leftCast = left;
          rightCast = right;
        }

        assert leftCast != null;
        IBooleanItem result = compare(leftCast, operator, rightCast);
        if (IBooleanItem.TRUE.equals(result)) {
          retval = IBooleanItem.TRUE;
        }
      }
    }
    return retval;
  }

  /**
   * Attempts to cast the provided {@code other} item to the type of the
   * {@code item}.
   *
   * @param item
   *          the item whose type the other item is to be cast to
   * @param other
   *          the item to cast
   * @return the casted item
   */
  @NonNull
  private static IAnyAtomicItem applyGeneralComparisonCast(@NonNull IAnyAtomicItem item,
      @NonNull IAnyAtomicItem other) {
    IAnyAtomicItem retval;
    if (item instanceof INumericItem) {
      retval = IDecimalItem.cast(other);
    } else if (item instanceof IDayTimeDurationItem) {
      retval = IDayTimeDurationItem.cast(other);
    } else if (item instanceof IDayTimeDurationItem) {
      retval = IYearMonthDurationItem.cast(other);
    } else {
      retval = item.getJavaTypeAdapter().cast(other);
    }
    return retval;
  }

  /**
   * Compare the {@code right} item with the {@code left} item using the specified
   * {@code operator}.
   *
   * @param left
   *          the value to compare against
   * @param operator
   *          the comparison operator
   * @param right
   *          the value to compare with
   * @return the comparison result
   */
  @NonNull
  public static IBooleanItem compare( // NOPMD - unavoidable
      @NonNull IAnyAtomicItem left,
      @NonNull Operator operator,
      @NonNull IAnyAtomicItem right) {
    @NonNull
    IBooleanItem retval;
    if (left instanceof IStringItem || right instanceof IStringItem) {
      retval = stringCompare(IStringItem.cast(left), operator, IStringItem.cast(right));
    } else if (left instanceof INumericItem && right instanceof INumericItem) {
      retval = numericCompare((INumericItem) left, operator, (INumericItem) right);
    } else if (left instanceof IBooleanItem && right instanceof IBooleanItem) {
      retval = booleanCompare((IBooleanItem) left, operator, (IBooleanItem) right);
    } else if (left instanceof IDateTimeItem && right instanceof IDateTimeItem) {
      retval = dateTimeCompare((IDateTimeItem) left, operator, (IDateTimeItem) right);
    } else if (left instanceof IDateItem && right instanceof IDateItem) {
      retval = dateCompare((IDateItem) left, operator, (IDateItem) right);
    } else if (left instanceof IDurationItem && right instanceof IDurationItem) {
      retval = durationCompare((IDurationItem) left, operator, (IDurationItem) right);
    } else if (left instanceof IBase64BinaryItem && right instanceof IBase64BinaryItem) {
      retval = binaryCompare((IBase64BinaryItem) left, operator, (IBase64BinaryItem) right);
    } else {
      throw new InvalidTypeMetapathException(
          null,
          String.format("invalid types for comparison: %s %s %s", left.getClass().getName(),
              operator.name().toLowerCase(Locale.ROOT), right.getClass().getName()));
    }
    return retval;
  }

  /**
   * Perform a string-based comparison of the {@code right} item against the
   * {@code left} item using the specified {@code operator}.
   *
   * @param left
   *          the value to compare against
   * @param operator
   *          the comparison operator
   * @param right
   *          the value to compare with
   * @return the comparison result
   */
  @NonNull
  public static IBooleanItem stringCompare(@NonNull IStringItem left, @NonNull Operator operator,
      @NonNull IStringItem right) {
    int result = left.compareTo(right);
    boolean retval;
    switch (operator) {
    case EQ:
      // retval = OperationFunctions.opNumericEqual(left.compare(right),
      // IIntegerItem.ZERO);
      retval = result == 0;
      break;
    case GE:
      // retval = OperationFunctions.opNumericGreaterThan(left.compare(right),
      // IIntegerItem.NEGATIVE_ONE);
      retval = result >= 0;
      break;
    case GT:
      // retval = OperationFunctions.opNumericGreaterThan(left.compare(right),
      // IIntegerItem.ZERO);
      retval = result > 0;
      break;
    case LE:
      // retval = OperationFunctions.opNumericLessThan(left.compare(right),
      // IIntegerItem.ONE);
      retval = result <= 0;
      break;
    case LT:
      // retval = OperationFunctions.opNumericLessThan(left.compare(right),
      // IIntegerItem.ZERO);
      retval = result < 0;
      break;
    case NE:
      // retval = FnNot.fnNot(OperationFunctions.opNumericEqual(left.compare(right),
      // IIntegerItem.ZERO));
      retval = result != 0;
      break;
    default:
      throw new IllegalArgumentException(
          String.format("Unsupported operator '%s'", operator.name())); // NOPMD
    }
    return IBooleanItem.valueOf(retval);
  }

  /**
   * Perform a number-based comparison of the {@code right} item against the
   * {@code left} item using the specified {@code operator}.
   *
   * @param left
   *          the value to compare against
   * @param operator
   *          the comparison operator
   * @param right
   *          the value to compare with
   * @return the comparison result
   */
  @NonNull
  public static IBooleanItem numericCompare(@NonNull INumericItem left, @NonNull Operator operator,
      @NonNull INumericItem right) {
    IBooleanItem retval;
    switch (operator) {
    case EQ:
      retval = OperationFunctions.opNumericEqual(left, right);
      break;
    case GE: {
      IBooleanItem gt = OperationFunctions.opNumericGreaterThan(left, right);
      IBooleanItem eq = OperationFunctions.opNumericEqual(left, right);
      retval = IBooleanItem.valueOf(gt.toBoolean() || eq.toBoolean());
      break;
    }
    case GT:
      retval = OperationFunctions.opNumericGreaterThan(left, right);
      break;
    case LE: {
      IBooleanItem lt = OperationFunctions.opNumericLessThan(left, right);
      IBooleanItem eq = OperationFunctions.opNumericEqual(left, right);
      retval = IBooleanItem.valueOf(lt.toBoolean() || eq.toBoolean());
      break;
    }
    case LT:
      retval = OperationFunctions.opNumericLessThan(left, right);
      break;
    case NE:
      retval = FnNot.fnNot(OperationFunctions.opNumericEqual(left, right));
      break;
    default:
      throw new IllegalArgumentException(String.format("Unsupported operator '%s'", operator.name()));
    }
    return retval;
  }

  /**
   * Perform a boolean-based comparison of the {@code right} item against the
   * {@code left} item using the specified {@code operator}.
   *
   * @param left
   *          the value to compare against
   * @param operator
   *          the comparison operator
   * @param right
   *          the value to compare with
   * @return the comparison result
   */
  @NonNull
  public static IBooleanItem booleanCompare(@NonNull IBooleanItem left, @NonNull Operator operator,
      @NonNull IBooleanItem right) {
    IBooleanItem retval;
    switch (operator) {
    case EQ:
      retval = OperationFunctions.opBooleanEqual(left, right);
      break;
    case GE: {
      IBooleanItem gt = OperationFunctions.opBooleanGreaterThan(left, right);
      IBooleanItem eq = OperationFunctions.opBooleanEqual(left, right);
      retval = IBooleanItem.valueOf(gt.toBoolean() || eq.toBoolean());
      break;
    }
    case GT:
      retval = OperationFunctions.opBooleanGreaterThan(left, right);
      break;
    case LE: {
      IBooleanItem lt = OperationFunctions.opBooleanLessThan(left, right);
      IBooleanItem eq = OperationFunctions.opBooleanEqual(left, right);
      retval = IBooleanItem.valueOf(lt.toBoolean() || eq.toBoolean());
      break;
    }
    case LT:
      retval = OperationFunctions.opBooleanLessThan(left, right);
      break;
    case NE:
      retval = FnNot.fnNot(OperationFunctions.opBooleanEqual(left, right));
      break;
    default:
      throw new IllegalArgumentException(String.format("Unsupported operator '%s'", operator.name()));
    }
    return retval;
  }

  /**
   * Perform a date and time-based comparison of the {@code right} item against
   * the {@code left} item using the specified {@code operator}.
   *
   * @param left
   *          the value to compare against
   * @param operator
   *          the comparison operator
   * @param right
   *          the value to compare with
   * @return the comparison result
   */
  @NonNull
  public static IBooleanItem dateTimeCompare(@NonNull IDateTimeItem left, @NonNull Operator operator,
      @NonNull IDateTimeItem right) {
    IBooleanItem retval;
    switch (operator) {
    case EQ:
      retval = OperationFunctions.opDateTimeEqual(left, right);
      break;
    case GE: {
      IBooleanItem gt = OperationFunctions.opDateTimeGreaterThan(left, right);
      IBooleanItem eq = OperationFunctions.opDateTimeEqual(left, right);
      retval = IBooleanItem.valueOf(gt.toBoolean() || eq.toBoolean());
      break;
    }
    case GT:
      retval = OperationFunctions.opDateTimeGreaterThan(left, right);
      break;
    case LE: {
      IBooleanItem lt = OperationFunctions.opDateTimeLessThan(left, right);
      IBooleanItem eq = OperationFunctions.opDateTimeEqual(left, right);
      retval = IBooleanItem.valueOf(lt.toBoolean() || eq.toBoolean());
      break;
    }
    case LT:
      retval = OperationFunctions.opDateTimeLessThan(left, right);
      break;
    case NE:
      retval = FnNot.fnNot(OperationFunctions.opDateTimeEqual(left, right));
      break;
    default:
      throw new IllegalArgumentException(String.format("Unsupported operator '%s'", operator.name()));
    }
    return retval;
  }

  /**
   * Perform a date-based comparison of the {@code right} item against the
   * {@code left} item using the specified {@code operator}.
   *
   * @param left
   *          the value to compare against
   * @param operator
   *          the comparison operator
   * @param right
   *          the value to compare with
   * @return the comparison result
   */
  @NonNull
  public static IBooleanItem dateCompare(@NonNull IDateItem left, @NonNull Operator operator,
      @NonNull IDateItem right) {
    IBooleanItem retval;
    switch (operator) {
    case EQ:
      retval = OperationFunctions.opDateEqual(left, right);
      break;
    case GE: {
      IBooleanItem gt = OperationFunctions.opDateGreaterThan(left, right);
      IBooleanItem eq = OperationFunctions.opDateEqual(left, right);
      retval = IBooleanItem.valueOf(gt.toBoolean() || eq.toBoolean());
      break;
    }
    case GT:
      retval = OperationFunctions.opDateGreaterThan(left, right);
      break;
    case LE: {
      IBooleanItem lt = OperationFunctions.opDateLessThan(left, right);
      IBooleanItem eq = OperationFunctions.opDateEqual(left, right);
      retval = IBooleanItem.valueOf(lt.toBoolean() || eq.toBoolean());
      break;
    }
    case LT:
      retval = OperationFunctions.opDateLessThan(left, right);
      break;
    case NE:
      retval = FnNot.fnNot(OperationFunctions.opDateEqual(left, right));
      break;
    default:
      throw new IllegalArgumentException(String.format("Unsupported operator '%s'", operator.name()));
    }
    return retval;
  }

  /**
   * Perform a duration-based comparison of the {@code right} item against the
   * {@code left} item using the specified {@code operator}.
   *
   * @param left
   *          the value to compare against
   * @param operator
   *          the comparison operator
   * @param right
   *          the value to compare with
   * @return the comparison result
   */
  @NonNull
  public static IBooleanItem durationCompare( // NOPMD - unavoidable
      @NonNull IDurationItem left,
      @NonNull Operator operator,
      @NonNull IDurationItem right) {
    IBooleanItem retval = null;
    switch (operator) {
    case EQ:
      retval = OperationFunctions.opDurationEqual(left, right);
      break;
    case GE:
      if (left instanceof IYearMonthDurationItem && right instanceof IYearMonthDurationItem) {
        IBooleanItem gt = OperationFunctions.opYearMonthDurationGreaterThan(
            (IYearMonthDurationItem) left,
            (IYearMonthDurationItem) right);
        IBooleanItem eq = OperationFunctions.opDurationEqual(left, right);
        retval = IBooleanItem.valueOf(gt.toBoolean() || eq.toBoolean());
      } else if (left instanceof IDayTimeDurationItem && right instanceof IDayTimeDurationItem) {
        IBooleanItem gt = OperationFunctions.opDayTimeDurationGreaterThan(
            (IDayTimeDurationItem) left,
            (IDayTimeDurationItem) right);
        IBooleanItem eq = OperationFunctions.opDurationEqual(left, right);
        retval = IBooleanItem.valueOf(gt.toBoolean() || eq.toBoolean());
      }
      break;
    case GT:
      if (left instanceof IYearMonthDurationItem && right instanceof IYearMonthDurationItem) {
        retval = OperationFunctions.opYearMonthDurationGreaterThan(
            (IYearMonthDurationItem) left,
            (IYearMonthDurationItem) right);
      } else if (left instanceof IDayTimeDurationItem && right instanceof IDayTimeDurationItem) {
        retval = OperationFunctions.opDayTimeDurationGreaterThan(
            (IDayTimeDurationItem) left,
            (IDayTimeDurationItem) right);
      }
      break;
    case LE:
      if (left instanceof IYearMonthDurationItem && right instanceof IYearMonthDurationItem) {
        IBooleanItem lt = OperationFunctions.opYearMonthDurationLessThan(
            (IYearMonthDurationItem) left,
            (IYearMonthDurationItem) right);
        IBooleanItem eq = OperationFunctions.opDurationEqual(left, right);
        retval = IBooleanItem.valueOf(lt.toBoolean() || eq.toBoolean());
      } else if (left instanceof IDayTimeDurationItem && right instanceof IDayTimeDurationItem) {
        IBooleanItem lt = OperationFunctions.opDayTimeDurationLessThan(
            (IDayTimeDurationItem) left,
            (IDayTimeDurationItem) right);
        IBooleanItem eq = OperationFunctions.opDurationEqual(left, right);
        retval = IBooleanItem.valueOf(lt.toBoolean() || eq.toBoolean());
      }
      break;
    case LT:
      if (left instanceof IYearMonthDurationItem && right instanceof IYearMonthDurationItem) {
        retval = OperationFunctions.opYearMonthDurationLessThan(
            (IYearMonthDurationItem) left,
            (IYearMonthDurationItem) right);
      } else if (left instanceof IDayTimeDurationItem && right instanceof IDayTimeDurationItem) {
        retval = OperationFunctions.opDayTimeDurationLessThan(
            (IDayTimeDurationItem) left,
            (IDayTimeDurationItem) right);
      }
      break;
    case NE:
      retval = FnNot.fnNot(OperationFunctions.opDurationEqual(left, right));
      break;
    default:
      throw new IllegalArgumentException(String.format("Unsupported operator '%s'", operator.name()));
    }

    if (retval == null) {
      throw new InvalidTypeMetapathException(
          null,
          String.format("The item types '%s' and '%s' are not comparable",
              left.getClass().getName(),
              right.getClass().getName()));
    }
    return retval;
  }

  /**
   * Perform a binary data-based comparison of the {@code right} item against the
   * {@code left} item using the specified {@code operator}.
   *
   * @param left
   *          the value to compare against
   * @param operator
   *          the comparison operator
   * @param right
   *          the value to compare with
   * @return the comparison result
   */
  @NonNull
  public static IBooleanItem binaryCompare(@NonNull IBase64BinaryItem left, @NonNull Operator operator,
      @NonNull IBase64BinaryItem right) {
    IBooleanItem retval;
    switch (operator) {
    case EQ:
      retval = OperationFunctions.opBase64BinaryEqual(left, right);
      break;
    case GE: {
      IBooleanItem gt = OperationFunctions.opBase64BinaryGreaterThan(left, right);
      IBooleanItem eq = OperationFunctions.opBase64BinaryEqual(left, right);
      retval = IBooleanItem.valueOf(gt.toBoolean() || eq.toBoolean());
      break;
    }
    case GT:
      retval = OperationFunctions.opBase64BinaryGreaterThan(left, right);
      break;
    case LE: {
      IBooleanItem lt = OperationFunctions.opBase64BinaryLessThan(left, right);
      IBooleanItem eq = OperationFunctions.opBase64BinaryEqual(left, right);
      retval = IBooleanItem.valueOf(lt.toBoolean() || eq.toBoolean());
      break;
    }
    case LT:
      retval = OperationFunctions.opBase64BinaryLessThan(left, right);
      break;
    case NE:
      retval = FnNot.fnNot(OperationFunctions.opBase64BinaryEqual(left, right));
      break;
    default:
      throw new IllegalArgumentException(String.format("Unsupported operator '%s'", operator.name()));
    }
    return retval;
  }

  /**
   * Compare the {@code right} item with the {@code left} item using the specified
   * {@code operator}.
   *
   * @param left
   *          the value to compare against
   * @param right
   *          the value to compare with
   * @return the comparison result
   */
  @NonNull
  public static IIntegerItem compareTo(
      @NonNull IAnyAtomicItem left,
      @NonNull IAnyAtomicItem right) {
    return IIntegerItem.valueOf(left.compareTo(right));
  }
}