/** * Copyright (C) 2012 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.financial.analytics.timeseries; import org.apache.commons.lang.ObjectUtils; import org.threeten.bp.LocalDate; import org.threeten.bp.Period; import com.opengamma.OpenGammaRuntimeException; import com.opengamma.engine.function.FunctionExecutionContext; import com.opengamma.util.ArgumentChecker; import com.opengamma.util.time.DateUtils; import com.opengamma.util.tuple.Pair; import com.opengamma.util.tuple.Pairs; /** * Utility class for building date constraints for the time series fetching functions. * <p> * Date constraint strings are crude expressions that are evaluated at execution time. This allows the valuation time to be referred to symbolically. * <dl> * <dt><em>YYYY</em>-</em>MM</em>-<em>DD</em></dt> * <dd>The date literal</dd> * <dt>Now</dt> * <dd>The valuation date</dd> * <dt>Null</dt> * <dd>Will return <code>null</code> from the {@link #getLocalDate} function</dd> * <dt>PreviousWeekDay(<em>expr</em>)</dt> * <dd>The previous weekday to the evaluated date constraint expression</dd> * <dt>PreviousWeekDay</dt> * <dd>The previous weekday to the valuation date. This is equivalent to <code>PreviousWeekDay(NOW)</code></dd> * <dt>NextWeekDay(<em>expr</em>)</dt> * <dd>The next weekday to the evaluated date constraint expression</dd> * <dt><em>expr</em>[+|-]<em>period</em></dt> * <dd>The evaluated date constraint expression plus or minus the given period, for example <code>PreviousWeekDay-P7D</code></dd> * <dt>-<em>period</em></dt> * <dd>The valuation date minus the given period, for example <code>-P1D</code> for the previous day. This is equivalent to <code>NOW-<em>period</em></code></dd> * </dl> */ public abstract class DateConstraint { private static final String NOW_STRING = "Now"; private static final String NULL_STRING = "Null"; private static final String PREVIOUS_WEEK_DAY_STRING = "PreviousWeekDay"; private static final String NEXT_WEEK_DAY_STRING = "NextWeekDay"; /** * Constant for the "null" date constraint. Some of the time series APIs use null to mean the earliest available date. */ public static final DateConstraint NULL = new NullDateConstraint(); /** * Date constraint referring to the current valuation time. */ public static final DateConstraint VALUATION_TIME = new ValuationTime(); /* package */DateConstraint() { } public static DateConstraint of(final LocalDate date) { ArgumentChecker.notNull(date, "date"); return new LiteralDateConstraint(date); } /** * Returns a date constraint that corresponds to the weekday before this one. * * @return the new date constraint */ public DateConstraint previousWeekDay() { return new WeekDayDateConstraint(this, -1); } /** * Returns a date constraint that corresponds to the weekday after this one. * * @return the new date constraint */ public DateConstraint nextWeekDay() { return new WeekDayDateConstraint(this, 1); } /** * Returns a date constraint that corresponds to this one plus the given period. * * @param period the period to add, not null * @return the new date constraint */ public DateConstraint plus(final Period period) { return new PlusMinusPeriodDateConstraint(this, true, period); } /** * Returns a date constraint that corresponds to this one minus the given period. * * @param period the period to subtract, not null * @return the new date constraint */ public DateConstraint minus(final Period period) { return new PlusMinusPeriodDateConstraint(this, false, period); } /** * Returns a date constraint that corresponds to this one minus the given period. * * @param period the period to subtract, not null * @return the new date constraint */ public DateConstraint minus(final String period) { return minus(Period.parse(period)); } /** * Approximates the period difference between two constraints, that is the period that must be added to this contraint to get the same value as the other one. * * @param other the other constraint, not null * @return the difference as a period, not null * @throws IllegalArgumentException if the constraints are not sufficiently compatible */ public Period periodUntil(final DateConstraint other) { if (equals(other)) { return Period.ZERO; } else if (other instanceof PlusMinusPeriodDateConstraint) { return ((PlusMinusPeriodDateConstraint) other).periodUntil(this).negated(); } else { throw new IllegalArgumentException(other + " - " + this); } } @Override public abstract String toString(); private static final class NullDateConstraint extends DateConstraint { @Override public DateConstraint previousWeekDay() { throw new UnsupportedOperationException("Can't take previous week day from NULL"); } @Override public DateConstraint nextWeekDay() { throw new UnsupportedOperationException("Can't take next week day from NULL"); } @Override public DateConstraint plus(final Period period) { throw new UnsupportedOperationException("Can't add a period to NULL"); } @Override public DateConstraint minus(final Period period) { throw new UnsupportedOperationException("Can't subtract a period from NULL"); } @Override public String toString() { return NULL_STRING; } @Override public int hashCode() { return 0; } @Override public boolean equals(final Object o) { return (o instanceof NullDateConstraint); } } private static final class LiteralDateConstraint extends DateConstraint { private final LocalDate _value; public LiteralDateConstraint(final LocalDate value) { _value = value; } @Override public DateConstraint previousWeekDay() { return new LiteralDateConstraint(DateUtils.previousWeekDay(_value)); } @Override public DateConstraint nextWeekDay() { return new LiteralDateConstraint(DateUtils.nextWeekDay(_value)); } @Override public DateConstraint plus(final Period period) { return new LiteralDateConstraint(_value.plus(period)); } @Override public DateConstraint minus(final Period period) { return new LiteralDateConstraint(_value.minus(period)); } @Override public Period periodUntil(final DateConstraint other) { if (other instanceof LiteralDateConstraint) { return _value.periodUntil(((LiteralDateConstraint) other)._value); } else { return super.periodUntil(other); } } @Override public String toString() { return _value.toString(); } @Override public int hashCode() { return _value.hashCode(); } @Override public boolean equals(final Object o) { if (!(o instanceof LiteralDateConstraint)) { return false; } final LiteralDateConstraint other = (LiteralDateConstraint) o; return _value.equals(other._value); } } private static final class PlusMinusPeriodDateConstraint extends DateConstraint { private final DateConstraint _underlying; private final boolean _plus; private final Period _period; public PlusMinusPeriodDateConstraint(final DateConstraint underlying, final boolean plus, final Period period) { _underlying = underlying; _plus = plus; _period = period; } @Override public DateConstraint plus(final Period period) { final Period newPeriod; if (_plus) { newPeriod = _period.plus(period); } else { newPeriod = _period.minus(period); } if (newPeriod.isZero()) { if (_underlying != null) { return _underlying; } else { return VALUATION_TIME; } } else { return new PlusMinusPeriodDateConstraint(_underlying, _plus, newPeriod); } } @Override public DateConstraint minus(final Period period) { final Period newPeriod; if (_plus) { newPeriod = _period.minus(period); } else { newPeriod = _period.plus(period); } if (newPeriod.isZero()) { if (_underlying != null) { return _underlying; } else { return VALUATION_TIME; } } else { return new PlusMinusPeriodDateConstraint(_underlying, _plus, newPeriod); } } @Override public Period periodUntil(final DateConstraint o) { if (o instanceof PlusMinusPeriodDateConstraint) { final PlusMinusPeriodDateConstraint other = (PlusMinusPeriodDateConstraint) o; if (ObjectUtils.equals(_underlying, other._underlying)) { final Period a = _plus ? _period : _period.negated(); final Period b = other._plus ? other._period : other._period.negated(); return b.minus(a); } } else if (o.equals((_underlying == null) ? DateConstraint.VALUATION_TIME : _underlying)) { if (_plus) { return _period.negated(); } else { return _period; } } throw new IllegalArgumentException(); } @Override public String toString() { final StringBuilder sb = new StringBuilder(); if (_underlying != null) { sb.append(_underlying); } sb.append(_plus ? '+' : '-'); sb.append(_period); return sb.toString(); } @Override public int hashCode() { return ObjectUtils.hashCode(_underlying) + (_plus ? 1 : 0) + ObjectUtils.hashCode(_period); } @Override public boolean equals(final Object o) { if (!(o instanceof PlusMinusPeriodDateConstraint)) { return false; } final PlusMinusPeriodDateConstraint other = (PlusMinusPeriodDateConstraint) o; return ObjectUtils.equals(_underlying, other._underlying) && (_plus == other._plus) && ObjectUtils.equals(_period, other._period); } } private static final class WeekDayDateConstraint extends DateConstraint { private final DateConstraint _underlying; private final int _adjust; public WeekDayDateConstraint(final DateConstraint underlying, final int adjust) { _underlying = underlying; _adjust = adjust; } @Override public DateConstraint previousWeekDay() { if (_adjust == 1) { if (_underlying != null) { return _underlying; } else { return VALUATION_TIME; } } else { return new WeekDayDateConstraint(_underlying, _adjust - 1); } } @Override public DateConstraint nextWeekDay() { if (_adjust == -1) { if (_underlying != null) { return _underlying; } else { return VALUATION_TIME; } } else { return new WeekDayDateConstraint(_underlying, _adjust + 1); } } private String expr(final int adjust, final String str) { if (adjust == 1) { if (_underlying != null) { return str + "(" + _underlying + ")"; } else { return str; } } else { return str + "(" + expr(adjust - 1, str) + ")"; } } @Override public String toString() { if (_adjust < 0) { return expr(-_adjust, PREVIOUS_WEEK_DAY_STRING); } else { return expr(_adjust, NEXT_WEEK_DAY_STRING); } } @Override public int hashCode() { return ObjectUtils.hashCode(_underlying) + _adjust; } @Override public boolean equals(final Object o) { if (!(o instanceof WeekDayDateConstraint)) { return false; } final WeekDayDateConstraint other = (WeekDayDateConstraint) o; return ObjectUtils.equals(_underlying, other._underlying) && (_adjust == other._adjust); } } private static final class ValuationTime extends DateConstraint { @Override public DateConstraint previousWeekDay() { return new WeekDayDateConstraint(null, -1); } @Override public DateConstraint nextWeekDay() { return new WeekDayDateConstraint(null, 1); } @Override public DateConstraint plus(final Period period) { return new PlusMinusPeriodDateConstraint(null, true, period); } @Override public DateConstraint minus(final Period period) { return new PlusMinusPeriodDateConstraint(null, false, period); } @Override public String toString() { return NOW_STRING; } @Override public int hashCode() { return Integer.MAX_VALUE; } @Override public boolean equals(final Object o) { return (o instanceof ValuationTime); } } private static Pair<String, String> parseBrackets(final String str) { if (str.length() == 0) { return Pairs.ofNulls(); } else if (str.charAt(0) == '(') { int index = 1; int count = 1; do { switch (str.charAt(index++)) { case '(': count++; break; case ')': count--; break; } } while (count > 0); final String bracketExpr = str.substring(1, index - 1); if (index == str.length()) { return Pairs.of(bracketExpr, (String) null); } else { return Pairs.of(bracketExpr, str.substring(index)); } } else { return Pairs.of((String) null, str); } } private static DateConstraint parseRight(final DateConstraint left, final String str) { if (str.charAt(0) == '-') { return left.minus(Period.parse(str.substring(1))); } else if (str.charAt(0) == '+') { return left.plus(Period.parse(str.substring(1))); } else { throw new IllegalArgumentException("Can't parse tail expression " + str + " of " + left); } } private static LocalDate evaluateRight(final LocalDate left, final String str) { if (str.charAt(0) == '-') { return left.minus(Period.parse(str.substring(1))); } else if (str.charAt(0) == '+') { return left.plus(Period.parse(str.substring(1))); } else { throw new IllegalArgumentException("Can't parse tail expression " + str + " of " + left); } } /** * Basic parsing of a date constraint string to a {@link DateConstraint} object. * <p> * This is not a full parser for the syntax described above. For example, expressions such as <code>-P7D-P7D</code> will not be recognized. Such expressions will not however be constructed using the * classes above (it would produce <code>-P14D</code>). * * @param str the string to parse, not null * @return the parsed constraint or null if the empty string is given */ public static DateConstraint parse(final String str) { if (str.length() == 0) { return null; } try { if (str.startsWith(NOW_STRING)) { if (str.length() == NOW_STRING.length()) { return VALUATION_TIME; } else { return parseRight(VALUATION_TIME, str.substring(NOW_STRING.length())); } } else if (str.startsWith(NULL_STRING)) { return NULL; } else if (str.charAt(0) == '-') { return VALUATION_TIME.minus(Period.parse(str.substring(1))); } else if (str.charAt(0) == '+') { return VALUATION_TIME.plus(Period.parse(str.substring(1))); } else if (str.startsWith(PREVIOUS_WEEK_DAY_STRING)) { final Pair<String, String> brackets = parseBrackets(str.substring(PREVIOUS_WEEK_DAY_STRING.length())); final DateConstraint left; if (brackets.getFirst() != null) { left = parse(brackets.getFirst()).previousWeekDay(); } else { left = VALUATION_TIME.previousWeekDay(); } if (brackets.getSecond() != null) { return parseRight(left, brackets.getSecond()); } else { return left; } } else if (str.startsWith(NEXT_WEEK_DAY_STRING)) { final Pair<String, String> brackets = parseBrackets(str.substring(NEXT_WEEK_DAY_STRING.length())); final DateConstraint left; if (brackets.getFirst() != null) { left = parse(brackets.getFirst()).nextWeekDay(); } else { left = VALUATION_TIME.nextWeekDay(); } if (brackets.getSecond() != null) { return parseRight(left, brackets.getSecond()); } else { return left; } } else { return new LiteralDateConstraint(LocalDate.parse(str)); } } catch (final Exception e) { throw new OpenGammaRuntimeException("Unable to parse date constraint '" + str + "'", e); } } /** * Evaluates a date constraint expression with respect to the information in the execution context such as the valuation time. * <p> * This is more efficient than parsing and evaluating the {@link DateConstraint} object structures as two separate steps. * * @param context the execution context, not null * @param str the string to parse and evaluate * @return the evaluated local date, possibly null */ public static LocalDate evaluate(final FunctionExecutionContext context, final String str) { if (str.length() == 0) { return null; } if (str.startsWith(NOW_STRING)) { if (str.length() == NOW_STRING.length()) { return valuationTime(context); } else { return evaluateRight(valuationTime(context), str.substring(NOW_STRING.length())); } } else if (str.startsWith(NULL_STRING)) { return null; } else if (str.charAt(0) == '-') { return valuationTime(context).minus(Period.parse(str.substring(1))); } else if (str.charAt(0) == '+') { return valuationTime(context).plus(Period.parse(str.substring(1))); } else if (str.startsWith(PREVIOUS_WEEK_DAY_STRING)) { final Pair<String, String> brackets = parseBrackets(str.substring(PREVIOUS_WEEK_DAY_STRING.length())); final LocalDate left; if (brackets.getFirst() != null) { left = DateUtils.previousWeekDay(evaluate(context, brackets.getFirst())); } else { left = DateUtils.previousWeekDay(valuationTime(context)); } if (brackets.getSecond() != null) { return evaluateRight(left, brackets.getSecond()); } else { return left; } } else if (str.startsWith(NEXT_WEEK_DAY_STRING)) { final Pair<String, String> brackets = parseBrackets(str.substring(NEXT_WEEK_DAY_STRING.length())); final LocalDate left; if (brackets.getFirst() != null) { left = DateUtils.nextWeekDay(evaluate(context, brackets.getFirst())); } else { left = DateUtils.nextWeekDay(valuationTime(context)); } if (brackets.getSecond() != null) { return evaluateRight(left, brackets.getSecond()); } else { return left; } } else { return LocalDate.parse(str); } } private static LocalDate valuationTime(final FunctionExecutionContext context) { return LocalDate.now(context.getValuationClock()); } }