Date-shifting

Depending on your business requirements, you may need to create a timeline based from another timeline, where the dates of value change in the resultant timeline are different from those in the input timeline.

CER does not include any expressions for date-shifting, as the types of date-shifting required tend to be business specific. The recommended approach is to create a static Java method to create your required timeline and invoke the static method from rules by use of the call expression.

Important: When implementing a date-shifting algorithm, take care to ensure that there is no attempt to create a timeline with more than one value on any given date, as such an attempt will fail at runtime.

The tests for your algorithm should include any tests for edge cases, such as leap-years or months which have different numbers of days.

Date addition example

You have a business requirement as follows: a person may not apply for a type of benefit within three months of receiving that benefit.

To implement this business requirement, you already have a timeline isReceivingBenefitTimeline that shows the periods of time for which a person is receiving benefit.

You now need another timeline isDisallowedFromApplyingForBenefitTimeline which shows the periods when it is invalid for that person to reapply for the benefit. This timeline is a date-addition of 3 months to the value-change dates in isReceivingBenefitTimeline:

Figure 1. A Requirement for a Date-Addition TimelineTimeline example.

Here is a sample implementation of a static method which can be called from CER rules:

package curam.creole.example;

import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import curam.creole.execution.session.Session;
import curam.creole.value.Interval;
import curam.creole.value.Timeline;
import curam.util.type.Date;

public class DateAdditionTimeline {

  /**
   * Creates a Timeline based on the input timeline, with the date
   * shifted by the number of months specified.
   * <p>
   * Note that the timeline's parameter can be of any type.
   *
   * @param session
   *          the CER session
   * @param inputTimeline
   *          the timeline whose dates must be shifted
   * @param monthsToAdd
   *          the number of months to add to the timeline change
   *          dates
   * @param <VALUE>
   *          the type of value held in the input/output timelines
   * @return a new timeline with the values from the input
   *         timeline, shifted by the number of months specified
   */
  public static <VALUE> Timeline<VALUE> addMonthsTimeline(
      final Session session, final Timeline<VALUE> inputTimeline,
      final Number monthsToAdd) {

    /*
     * CER will typically pass a Number, which must be converted to
     * an integer
     */
    final int monthsToAddInteger = monthsToAdd.intValue();

    /*
     * Find the intervals within the input timeline
     */
    final List<? extends Interval<VALUE>> inputIntervals =
        inputTimeline.intervals();

    /*
     * Amass the output intervals. Note that we map by start date,
     * because when adding months, it is possible for several
     * different input dates to be shifted to the same output date.
     *
     * For example 3 months after these dates: 2002-11-28,
     * 2002-11-29, 2002-11-30, are all calculated as 2003-02-28
     *
     * In this situation, we use the value from the earliest input
     * date only - input dates are processed in ascending order
     */
    final Map<Date, Interval<VALUE>> outputIntervalsMap =
        new HashMap<Date, Interval<VALUE>>(inputIntervals.size());

    for (final Interval<VALUE> inputInterval : inputIntervals) {
      // get the interval start date
      final Date inputStartDate = inputInterval.startDate();

      /*
       * Add the number of months - but n months after the start of
       * time is still the start of time
       */

      final Date outputStartDate;
      if (inputStartDate == null) {
        outputStartDate = null;
      } else {
        final Calendar startDateCalendar =
            inputStartDate.getCalendar();

        startDateCalendar.add(Calendar.MONTH, monthsToAddInteger);
        outputStartDate = new Date(startDateCalendar);

      }

      // check that this output date has not yet been processed
      if (!outputIntervalsMap.containsKey(outputStartDate)) {

        /*
         * the output interval uses the same value as the input
         * interval, but with a shifted start date
         */

        final Interval<VALUE> outputInterval =
            new Interval<VALUE>(outputStartDate,
                inputInterval.value());
        outputIntervalsMap.put(outputStartDate, outputInterval);
      }
    }

    // create a timeline from the output intervals
    final Collection<Interval<VALUE>> outputIntervals =
        outputIntervalsMap.values();
    final Timeline<VALUE> outputTimeline =
        new Timeline<VALUE>(outputIntervals);
    return outputTimeline;

  }
}

Date spreading example

You have a business requirement as follows: a car must be taxed for any month where the car is "on the road" for one or more days in that month1.

To implement this business requirement, you already have a timeline isOnRoadTimeline that shows the periods of time for which a car is "on the road".

You now need another timeline taxDueTimeline which shows the periods when the car must be taxed. This timeline is a spread-out of the dates within isOnRoadTimeline:

Figure 2. A Requirement for a Date-Spreading TimelineTimeline example.

Here is a sample implementation of a static method which can be called from CER rules:

package curam.creole.example;

import java.util.Calendar;
import java.util.Collection;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import curam.creole.execution.session.Session;
import curam.creole.value.Interval;
import curam.creole.value.Timeline;
import curam.util.type.Date;

public class DateSpreadingTimeline {

  /**
   * Creates a Timeline for the period for which a car must be
   * taxed.
   * <p>
   * The car must be taxed for the entire month for any month where
   * that car is on-the-road for one or more days during that
   * month.
   */
  public static Timeline<Boolean> taxDue(final Session session,
      final Timeline<Boolean> isOnRoadTimeline) {

    /*
     * Find the intervals within the input timeline
     */
    final List<? extends Interval<Boolean>> isOnRoadIntervals =
        isOnRoadTimeline.intervals();

    /*
     * Amass the output intervals. Note that we map by start date;
     * a car may go off the road during a month, which would imply
     * that no tax is required at the start of the next month, only
     * to return to the road part-way through the next month, in
     * which case it does require taxing after all.
     *
     * For example, car is put back on the road 2001-01-15, so tax
     * is required (retrospectively) from 2001-01-01.
     *
     * On 2001-01-24 the car is taken back off the road, so it's
     * possible that the car does not require taxing from
     * 2001-02-01.
     *
     * However, on 2001-02-05 the car is put back on the road, and
     * so it does require taxing from 2001-02-01 after all. The
     * resultant timeline will merge these periods to show that the
     * car requires taxing from 2001-01-01 onwards (thus covering
     * from 2001-02-01 too).
     */
    final Map<Date, Interval<Boolean>> taxDueIntervalsMap =
        new HashMap<Date, Interval<Boolean>>(
            isOnRoadIntervals.size());

    for (final Interval<Boolean> isOnRoadInterval :
 isOnRoadIntervals) {
      // get the interval start date
      final Date isOnRoadStartDate = isOnRoadInterval.startDate();

      if (isOnRoadStartDate == null) {
        // at the start of time, the car must be taxed if it is on
        // the road
        taxDueIntervalsMap.put(null, new Interval<Boolean>(null,
            isOnRoadInterval.value()));
      } else if (isOnRoadInterval.value()) {
        /*
         * start of a period of the car being on-the-road - the car
         * must be taxed from the start of the month containing the
         * start of this period
         */

        final Calendar carOnRoadStartCalendar =
            isOnRoadStartDate.getCalendar();
        final Calendar startOfMonthCalendar =
            new GregorianCalendar(
                carOnRoadStartCalendar.get(Calendar.YEAR),
                carOnRoadStartCalendar.get(Calendar.MONTH), 1);
        final Date startOfMonthDate =
            new Date(startOfMonthCalendar);

        /*
         * Add to the map of tax due periods - note that this will
         * push out of the map any "tax not due" interval
         * speculatively added if the car went off-the-road during
         * the previous month
         */
        taxDueIntervalsMap.put(startOfMonthDate,
            new Interval<Boolean>(startOfMonthDate, true));
      } else {
        /*
         * Start of a period of the car being off the road -
         * speculate that from the start of next month, the car may
         * not require tax. This speculation will hold unless the
         * car is subsequently found to be put back on the road
         * next month, in which case this speculation will be
         * discarded (i.e. pushed out of the map).
         */

        final Calendar carOffRoadStartCalendar =
            isOnRoadStartDate.getCalendar();
        final Calendar startOfNextMonthCalendar =
            new GregorianCalendar(
                carOffRoadStartCalendar.get(Calendar.YEAR),
                carOffRoadStartCalendar.get(Calendar.MONTH), 1);
        startOfNextMonthCalendar.add(Calendar.MONTH, 1);

        final Date startOfNextMonthDate =
            new Date(startOfNextMonthCalendar);

        /*
         * Add to the map of tax due periods - note that this will
         * push out of the map any "tax not due" interval
         * speculatively added if the car went off-the-road during
         * the previous month
         */
        taxDueIntervalsMap.put(startOfNextMonthDate,
            new Interval<Boolean>(startOfNextMonthDate, false));
      }

    }

    // create a timeline from the tax due intervals
    final Collection<Interval<Boolean>> taxDueIntervals =
        taxDueIntervalsMap.values();
    final Timeline<Boolean> taxDueTimeline =
        new Timeline<Boolean>(taxDueIntervals);
    return taxDueTimeline;

  }
}
1 I.e. if a car is put back on the road part-way through a month, the keeper of the car must ensure that tax is retrospectively paid for the entire month.