TimeInstant.java

/*
 * Copyright 2014 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.dashbuilder.dataset.date;

import java.util.Date;

import org.dashbuilder.dataset.group.DateIntervalType;

/**
 * This class is used to represent a given time instant relative to the current time.
 * <p>Some examples of time instants are:
 * <ul>
 * <li><i>now</i></li>
 * <li><i>now -10second</i> or just <i>-10second</i></li>
 * <li><i>begin[minute]</i> => begin of current minute. It turns back the clock to first second.</li>
 * <li><i>begin[year March]</i> => begin of current year (year starting on March)</li>
 * <li><i>end[year March] +1year</i> => end of next year (year starting on March)</li>
 * <li><i>begin[year March] -7day</i> => last year's last week start</li>
 * </ul>
 * </p>
 */
public class TimeInstant {

    public enum TimeMode {
        NOW,
        BEGIN,
        END;

        private final static TimeMode[] modes = values();

        public int getIndex() {
            return ordinal();
        }

        public static TimeMode getByName(String name) {
            try {
                return valueOf(name.toUpperCase());
            } catch (Exception e) {
                return null;
            }
        }

        public static TimeMode getByIndex(int index) {
            return modes[index];
        }

    }

    private TimeMode timeMode = null;
    private DateIntervalType intervalType = DateIntervalType.YEAR;
    private Month firstMonthOfYear = Month.JANUARY;
    private TimeAmount timeAmount = null;

    /**
     * The date used as the relative time from which NOW based calculations must be done.
     */
    private transient Date startTime = null;

    public TimeInstant() {
        this(TimeMode.NOW, DateIntervalType.YEAR, Month.JANUARY, null);
    }

    public TimeInstant(TimeMode timeMode, DateIntervalType intervalType, Month firstMonthOfYear, TimeAmount timeAmount) {
        this.timeMode = timeMode;
        this.intervalType = intervalType;
        this.firstMonthOfYear = firstMonthOfYear;
        this.timeAmount = timeAmount;
    }

    public TimeMode getTimeMode() {
        return timeMode;
    }

    public void setTimeMode(TimeMode timeMode) {
        this.timeMode = timeMode;
    }

    public DateIntervalType getIntervalType() {
        return intervalType;
    }

    public void setIntervalType(DateIntervalType intervalType) {
        this.intervalType = intervalType;
    }

    public Month getFirstMonthOfYear() {
        return firstMonthOfYear;
    }

    public void setFirstMonthOfYear(Month firstMonthOfYear) {
        this.firstMonthOfYear = firstMonthOfYear;
    }

    public TimeAmount getTimeAmount() {
        return timeAmount;
    }

    public void setTimeAmount(TimeAmount timeAmount) {
        this.timeAmount = timeAmount;
    }

    public String toString() {
        StringBuilder out = new StringBuilder();
        if (timeMode != null) {
            out.append(timeMode.name().toLowerCase());

            if (!TimeMode.NOW.equals(timeMode)) {

                out.append("[").append(intervalType.name().toLowerCase());
                if (intervalType != null
                    && intervalType.getIndex() > DateIntervalType.MONTH.getIndex()
                    && firstMonthOfYear != null) {

                    out.append(" ").append(firstMonthOfYear.name().toLowerCase());
                }
                out.append("]");
            }
        }
        if (timeAmount != null && timeAmount.getQuantity() != 0) {
            if (out.length() > 0) out.append(" ");
            out.append(timeAmount);
        }
        if (out.length() == 0) {
            out.append(TimeMode.NOW.name().toLowerCase());
        }
        return out.toString();
    }

    public TimeInstant cloneInstance() {
        TimeInstant clone = new TimeInstant();
        clone.timeMode = timeMode;
        clone.intervalType = intervalType;
        clone.firstMonthOfYear = firstMonthOfYear;
        if (timeAmount != null) clone.timeAmount = timeAmount.cloneInstance();
        return clone;
    }

    public Date getTimeInstant() {
        Date _start = calculateStartTime();
        if (timeAmount != null) timeAmount.adjustDate(_start);
        return _start;
    }

    public void setStartTime(Date now) {
        this.startTime = now;
    }

    public static Date START_TIME = null;
    
    public Date getStartTime() {
        if (startTime == null) {
            if (START_TIME != null) return new Date(START_TIME.getTime());
            return new Date();
        }
        return new Date(startTime.getTime());
    }

    protected Date calculateStartTime() {
        Date startDate = getStartTime();
        if (timeMode == null || TimeMode.NOW.equals(timeMode)) {
            return startDate;
        }

        if (DateIntervalType.MILLENIUM.equals(intervalType)) {
            int base = startDate.getYear() / 1000;
            int inc =  TimeMode.END.equals(timeMode) ? 1 : 0;
            startDate.setYear((base + inc) * 1000);
            startDate.setMonth(firstMonthOfYear.getIndex()-1);
            startDate.setDate(1);
            startDate.setHours(0);
            startDate.setMinutes(0);
            startDate.setSeconds(0);
        }
        if (DateIntervalType.CENTURY.equals(intervalType)) {
            int base = startDate.getYear() / 100;
            int inc =  TimeMode.END.equals(timeMode) ? 1 : 0;
            startDate.setYear((base + inc) * 100);
            startDate.setMonth(firstMonthOfYear.getIndex()-1);
            startDate.setDate(1);
            startDate.setHours(0);
            startDate.setMinutes(0);
            startDate.setSeconds(0);
        }
        if (DateIntervalType.DECADE.equals(intervalType)) {
            int base = startDate.getYear() / 10;
            int inc =  TimeMode.END.equals(timeMode) ? 1 : 0;
            startDate.setYear((base + inc) * 10);
            startDate.setMonth(firstMonthOfYear.getIndex()-1);
            startDate.setDate(1);
            startDate.setHours(0);
            startDate.setMinutes(0);
            startDate.setSeconds(0);
        }
        if (DateIntervalType.YEAR.equals(intervalType)) {
            int month = startDate.getMonth();
            int firstMonth = firstMonthOfYear.getIndex()-1;
            int yearInc =  0;
            if (TimeMode.BEGIN.equals(timeMode)) yearInc = month < firstMonth ? -1 : 0;
            else yearInc = month < firstMonth ? 0 : 1;

            startDate.setYear(startDate.getYear() + yearInc);
            startDate.setMonth(firstMonth);
            startDate.setDate(1);
            startDate.setHours(0);
            startDate.setMinutes(0);
            startDate.setSeconds(0);
        }
        if (DateIntervalType.QUARTER.equals(intervalType)) {
            int month = startDate.getMonth();
            int firstMonth = Quarter.getQuarterFirstMonth(firstMonthOfYear.getIndex(), month + 1)-1;
            int yearInc = 0;
            int monthInc = 3;
            if (TimeMode.BEGIN.equals(timeMode)) {
                yearInc = firstMonth>month ? -1 : 0;
                monthInc = 0;
            }
            startDate.setYear(startDate.getYear() + yearInc);
            startDate.setMonth(firstMonth + monthInc);
            startDate.setDate(1);
            startDate.setHours(0);
            startDate.setMinutes(0);
            startDate.setSeconds(0);
        }
        if (DateIntervalType.MONTH.equals(intervalType)) {
            startDate.setDate(1);
            startDate.setHours(0);
            startDate.setMinutes(0);
            startDate.setSeconds(0);
            if (TimeMode.END.equals(timeMode)) {
                startDate.setMonth(startDate.getMonth()+1);
            }
        }
        if (DateIntervalType.DAY.equals(intervalType)) {
            startDate.setHours(0);
            startDate.setMinutes(0);
            startDate.setSeconds(0);
            if (TimeMode.END.equals(timeMode)) {
                startDate.setDate(startDate.getDate()+1);
            }
        }
        if (DateIntervalType.HOUR.equals(intervalType)) {
            startDate.setMinutes(0);
            startDate.setSeconds(0);
            if (TimeMode.END.equals(timeMode)) {
                startDate.setHours(startDate.getHours()+1);
            }
        }
        if (DateIntervalType.MINUTE.equals(intervalType)) {
            startDate.setSeconds(0);
            if (TimeMode.END.equals(timeMode)) {
                startDate.setMinutes(startDate.getMinutes()+1);
            }
        }
        return startDate;
    }

    /**
     * Return a time instant representing the current time.
     */
    public static TimeInstant now() {
        return new TimeInstant();
    }

    /**
     * Parses a time instant expression.
     *
     * @param timeInstantExpr A valid time instant expression (<i>see TimeInstant class javadoc</i>)
     * @return A TimeInstant instance
     * @throws IllegalArgumentException If the expression is not valid
     */
    public static TimeInstant parse(String timeInstantExpr) {
        if (timeInstantExpr == null || timeInstantExpr.length() == 0) {
            throw new IllegalArgumentException("Empty time instant expression");
        }
        TimeInstant instant = new TimeInstant();
        String expr = timeInstantExpr.toLowerCase().trim();

        // now + time amount (optional)
        boolean begin = expr.startsWith("begin");
        boolean end  = expr.startsWith("end");
        if (!begin && !end) {
            if (expr.startsWith("now")) {
                instant.setTimeMode(TimeMode.NOW);
                if (expr.length() > 3) {
                    instant.setTimeAmount(TimeAmount.parse(expr.substring(3)));
                }
            } else {
                instant.setTimeMode(null);
                instant.setTimeAmount(TimeAmount.parse(expr));
            }
            return instant;
        }
        // begin/end modes
        instant.setTimeMode(begin ? TimeMode.BEGIN : TimeMode.END);

        // Look for braces limits "begin[year March]"
        String example = begin ? "begin[year March]" : "end[year March]";
        int bracesBegin = expr.indexOf('[');
        int bracesEnd = expr.indexOf(']');
        if (bracesBegin == -1 || bracesEnd == -1 || bracesBegin >= bracesEnd) {
            throw new IllegalArgumentException("Missing braces (ex '" + example + "'):  " + timeInstantExpr);
        }
        // Interval type
        String[] intervalTerms = expr.substring(bracesBegin+1, bracesEnd).split("\\s+");
        if (intervalTerms.length > 2) {
            throw new IllegalArgumentException("Too many settings (ex '" + example + "'):  " + timeInstantExpr);
        }
        instant.setIntervalType(DateIntervalType.getByName(intervalTerms[0]));
        if (instant.getIntervalType() == null) {
            throw new IllegalArgumentException("Invalid interval (ex '" + example + "'): " + timeInstantExpr);
        }

        // First month of year
        if (intervalTerms.length == 2) {
            instant.setFirstMonthOfYear(Month.getByName(intervalTerms[1]));
            if (instant.getFirstMonthOfYear() == null) {
                throw new IllegalArgumentException("Invalid first year month (ex '" + example + "'): " + timeInstantExpr);
            }
        }
        // Time amount
        if (bracesEnd < expr.length()) {
            expr = expr.substring(bracesEnd + 1).trim();
            if (!expr.isEmpty()) {
                TimeAmount timeAmount = TimeAmount.parse(expr);
                instant.setTimeAmount(timeAmount);
            }
        }
        return instant;
    }
}