com.ikanow.aleph2.data_model.utils.TimeUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.ikanow.aleph2.data_model.utils.TimeUtils.java

Source

/*******************************************************************************
 * Copyright 2015, The IKANOW Open Source Project.
 *
 * 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 com.ikanow.aleph2.data_model.utils;

import java.util.Arrays;
import java.util.Date;
import java.util.Optional;
import java.text.ParseException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang.time.DateUtils;

import scala.Tuple2;

import com.joestelmach.natty.CalendarSource;
import com.joestelmach.natty.DateGroup;
import com.joestelmach.natty.Parser;

import fj.data.Validation;

/** A collection of temporal utilities
 * @author Alex
 */
public class TimeUtils {

    /** The simplest date parsing utility - only handles daily/hourly/monthly type strings (1d, d, daily, day - etc). Note "m" is ambiguous and not supported, use "min" or "month"
     * @param human_readable_period - daily/hourly/monthly type strings (1d, d, daily, day - etc). Note "m" is ambiguous and not supported, use "min" or "month"
     * @return a ChronoUnit if successful, else a generic error string
     */
    public static Validation<String, ChronoUnit> getTimePeriod(final String human_readable_period) {
        return Patterns
                .match(Optional.ofNullable(human_readable_period).orElse("").toLowerCase().replaceAll("\\s+", ""))
                .<Validation<String, ChronoUnit>>andReturn()
                .when(d -> d.equals("1d"), __ -> Validation.success(ChronoUnit.DAYS))
                .when(d -> d.equals("d"), __ -> Validation.success(ChronoUnit.DAYS))
                .when(d -> d.equals("1day"), __ -> Validation.success(ChronoUnit.DAYS))
                .when(d -> d.equals("day"), __ -> Validation.success(ChronoUnit.DAYS))
                .when(d -> d.equals("daily"), __ -> Validation.success(ChronoUnit.DAYS))

                .when(d -> d.equals("1w"), __ -> Validation.success(ChronoUnit.WEEKS))
                .when(d -> d.equals("w"), __ -> Validation.success(ChronoUnit.WEEKS))
                .when(d -> d.equals("1wk"), __ -> Validation.success(ChronoUnit.WEEKS))
                .when(d -> d.equals("wk"), __ -> Validation.success(ChronoUnit.WEEKS))
                .when(d -> d.equals("1week"), __ -> Validation.success(ChronoUnit.WEEKS))
                .when(d -> d.equals("week"), __ -> Validation.success(ChronoUnit.WEEKS))
                .when(d -> d.equals("weekly"), __ -> Validation.success(ChronoUnit.WEEKS))

                .when(d -> d.equals("1month"), __ -> Validation.success(ChronoUnit.MONTHS))
                .when(d -> d.equals("month"), __ -> Validation.success(ChronoUnit.MONTHS))
                .when(d -> d.equals("monthly"), __ -> Validation.success(ChronoUnit.MONTHS))

                .when(d -> d.equals("1sec"), __ -> Validation.success(ChronoUnit.SECONDS))
                .when(d -> d.equals("sec"), __ -> Validation.success(ChronoUnit.SECONDS))
                .when(d -> d.equals("1s"), __ -> Validation.success(ChronoUnit.SECONDS))
                .when(d -> d.equals("s"), __ -> Validation.success(ChronoUnit.SECONDS))
                .when(d -> d.equals("1second"), __ -> Validation.success(ChronoUnit.SECONDS))
                .when(d -> d.equals("second"), __ -> Validation.success(ChronoUnit.SECONDS))

                .when(d -> d.equals("1min"), __ -> Validation.success(ChronoUnit.MINUTES))
                .when(d -> d.equals("min"), __ -> Validation.success(ChronoUnit.MINUTES))
                .when(d -> d.equals("1minute"), __ -> Validation.success(ChronoUnit.MINUTES))
                .when(d -> d.equals("minute"), __ -> Validation.success(ChronoUnit.MINUTES))

                .when(d -> d.equals("1h"), __ -> Validation.success(ChronoUnit.HOURS))
                .when(d -> d.equals("h"), __ -> Validation.success(ChronoUnit.HOURS))
                .when(d -> d.equals("1hour"), __ -> Validation.success(ChronoUnit.HOURS))
                .when(d -> d.equals("hour"), __ -> Validation.success(ChronoUnit.HOURS))
                .when(d -> d.equals("hourly"), __ -> Validation.success(ChronoUnit.HOURS))

                .when(d -> d.equals("1y"), __ -> Validation.success(ChronoUnit.YEARS))
                .when(d -> d.equals("y"), __ -> Validation.success(ChronoUnit.YEARS))
                .when(d -> d.equals("1year"), __ -> Validation.success(ChronoUnit.YEARS))
                .when(d -> d.equals("year"), __ -> Validation.success(ChronoUnit.YEARS))
                .when(d -> d.equals("1yr"), __ -> Validation.success(ChronoUnit.YEARS))
                .when(d -> d.equals("yr"), __ -> Validation.success(ChronoUnit.YEARS))
                .when(d -> d.equals("yearly"), __ -> Validation.success(ChronoUnit.YEARS))

                .otherwise(__ -> Validation
                        .fail(ErrorUtils.get(ErrorUtils.INVALID_DATETIME_FORMAT, human_readable_period)));
    }

    /** Returns the suffix of a time-based index given the grouping period
     * @param grouping_period - the grouping period
     * @param lowest_granularity
     * @return the index suffix, ie added to the base index
     */
    public static String getTimeBasedSuffix(final ChronoUnit grouping_period,
            final Optional<ChronoUnit> lowest_granularity) {
        return lowest_granularity
                .map(lg -> grouping_period.compareTo(lg) < 0 ? getTimeBasedSuffix(lg, Optional.empty()) : null)
                .orElse(Patterns.match(grouping_period).<String>andReturn()
                        .when(p -> ChronoUnit.SECONDS == p, __ -> "yyyy.MM.dd.HH:mm:ss")
                        .when(p -> ChronoUnit.MINUTES == p, __ -> "yyyy.MM.dd.HH:mm")
                        .when(p -> ChronoUnit.HOURS == p, __ -> "yyyy.MM.dd.HH")
                        .when(p -> ChronoUnit.DAYS == p, __ -> "yyyy.MM.dd")
                        .when(p -> ChronoUnit.WEEKS == p, __ -> "YYYY-ww") // (deliberately 'Y' (week-year) not 'y' since 'w' is week-of-year 
                        .when(p -> ChronoUnit.MONTHS == p, __ -> "yyyy.MM")
                        .when(p -> ChronoUnit.YEARS == p, __ -> "yyyy").otherwise(__ -> ""));
    }

    public final static String[] SUPPORTED_DATE_SUFFIXES = { "yyyy.MM.dd.HH:mm:ss", "yyyy.MM.dd.HH:mm",
            "yyyy.MM.dd.HH", "yyyy.MM.dd", "YYYY-ww", "yyyy.MM", "yyyy" };

    public final static ChronoUnit[] SUPPORTED_DATE_UNITS = { ChronoUnit.SECONDS, ChronoUnit.MINUTES,
            ChronoUnit.HOURS, ChronoUnit.DAYS, ChronoUnit.WEEKS, ChronoUnit.MONTHS, ChronoUnit.YEARS };

    public final static Pattern[] SUPPORTED_DATE_PATTERNS = {
            Pattern.compile("[0-9]{4}[.][0-9]{2}[.][0-9]{2}[.][0-9]{2}:[0-9]{2}:[0-9]{2}"),
            Pattern.compile("[0-9]{4}[.][0-9]{2}[.][0-9]{2}[.][0-9]{2}:[0-9]{2}"),
            Pattern.compile("[0-9]{4}[.][0-9]{2}[.][0-9]{2}[.][0-9]{2}"),
            Pattern.compile("[0-9]{4}[.][0-9]{2}[.][0-9]{2}"), Pattern.compile("[0-9]{4}-[0-9]{2}"),
            Pattern.compile("[0-9]{4}[.][0-9]{2}"), Pattern.compile("[0-9]{4}") };

    /** Returns a date representing an ISO date (with or without trailing "Z")
     * @param iso_date
     * @return
     */
    public static Validation<String, Date> parseIsoString(final String iso_date) {
        try {
            if (iso_date.endsWith("Z"))
                return Validation.success(Date.from(Instant.parse(iso_date)));
            return Validation.success(Date.from(Instant.parse(iso_date + "Z")));
        } catch (Exception e) {
            return Validation.fail(e.getMessage());
        }
    }

    /** Gets the date format from a date in one of the formats 
     * @param date_suffix
     * @return
     */
    public static Optional<Tuple2<String, ChronoUnit>> getFormatInfoFromDateString(final String date_suffix) {
        for (int ii = 0; ii < SUPPORTED_DATE_PATTERNS.length; ++ii) {
            if (SUPPORTED_DATE_PATTERNS[ii].matcher(date_suffix).matches()) {
                return Optional.of(Tuples._2T(SUPPORTED_DATE_SUFFIXES[ii], SUPPORTED_DATE_UNITS[ii]));
            }
        }
        return Optional.empty();
    }

    /** Returns the date corresponding to a string in one of the formats returned by getTimeBasedSuffix
     * @param suffix - the date string
     * @return - either the date, or an error if the string is not correctly formatted
     */
    public static Validation<String, Date> getDateFromSuffix(final String suffix) {
        try {
            return Validation.success(DateUtils.parseDateStrictly(suffix, SUPPORTED_DATE_SUFFIXES));
        } catch (ParseException e) {
            return Validation.fail(ErrorUtils.getLongForm("getDateFromSuffix {0}", e));
        }
    }

    /** Attempts to parse a (typically recurring) time  
     * @param human_readable_duration - Uses some simple regexes (1h,d, 1month etc), and Natty (try examples http://natty.joestelmach.com/try.jsp#)
     * @return the machine readable duration, or an error
     */
    public static Validation<String, Duration> getDuration(final String human_readable_duration) {
        return getDuration(human_readable_duration, Optional.empty());
    }

    /** Attempts to parse a (typically recurring) time  
     * @param human_readable_duration - Uses some simple regexes (1h,d, 1month etc), and Natty (try examples http://natty.joestelmach.com/try.jsp#)
     * @param base_date - for relative date, locks the date to this origin (mainly for testing in this case?)
     * @return the machine readable duration, or an error
     */
    public static Validation<String, Duration> getDuration(final String human_readable_duration,
            Optional<Date> base_date) {
        // There's a few different cases:
        // - the validation from getTimePeriod
        // - a slightly more complicated version <N><p> where <p> == period from the above
        // - use Natty for more complex expressions

        final Validation<String, ChronoUnit> first_attempt = getTimePeriod(human_readable_duration);
        if (first_attempt.isSuccess()) {
            return Validation
                    .success(Duration.of(first_attempt.success().getDuration().getSeconds(), ChronoUnit.SECONDS));
        } else { // Slightly more complex version
            final Matcher m = date_parser.matcher(human_readable_duration);
            if (m.matches()) {
                final Validation<String, Duration> candidate_ret = getTimePeriod(m.group(2)).map(cu -> {
                    final LocalDateTime now = LocalDateTime.now();
                    return Duration.between(now, now.plus(Integer.parseInt(m.group(1)), cu));
                });

                if (candidate_ret.isSuccess())
                    return candidate_ret;
            }
        }
        // If we're here then try Natty
        final Date now = base_date.orElse(new Date());
        return getSchedule(human_readable_duration, Optional.of(now)).map(d -> {
            final long duration = d.getTime() - now.getTime();
            return Duration.of(duration, ChronoUnit.MILLIS);
        });
    }

    /** Returns a date from a human readable date - can be in the future or past
     * @param human_readable_date - the date expressed in words, eg "next wednesday".. Uses some simple regexes (1h,d, 1month etc), and Natty (try examples http://natty.joestelmach.com/try.jsp#)
     * @param base_date - for relative date, locks the date to this origin
     * @return the machine readable date, or an error
     */
    public static Validation<String, Date> getSchedule(final String human_readable_date, Optional<Date> base_date) {
        try { // just read the first - note can ignore all the error checking here, just fail out using the try/catch
            final Date adjusted_date = base_date.orElse(new Date());
            CalendarSource.setBaseDate(adjusted_date);
            final Parser parser = new Parser();
            final List<DateGroup> l = parser.parse(human_readable_date);
            final DateGroup d = l.get(0);
            if (!d.getText().matches("^.*[a-zA-Z]+.*$")) { // only matches numbers, not allowed - must have missed a prefix
                return Validation.fail(ErrorUtils.get(ErrorUtils.INVALID_DATETIME_FORMAT, human_readable_date));
            }
            final List<Date> l2 = d.getDates();
            return Validation.success(l2.get(0));
        } catch (Exception e) {
            final Pattern numChronoPattern = Pattern.compile("^([\\d]+)(.*)");
            final Matcher m = numChronoPattern.matcher(human_readable_date);
            return m.find()
                    ? getTimePeriod(m.group(2)).map(c -> c.getDuration().get(ChronoUnit.SECONDS))
                            .map(l -> new Date(base_date.orElse(new Date()).getTime()
                                    + Long.parseLong(m.group(1)) * l * 1000L))
                    : getTimePeriod(human_readable_date).map(c -> c.getDuration().get(ChronoUnit.SECONDS))
                            .map(l -> new Date(base_date.orElse(new Date()).getTime() + l * 1000L));
        }
    }

    /** Returns a date from a human readable date - can only be in the future
     * @param human_readable_date - the date expressed in words, eg "next wednesday".. Uses some simple regexes (1h,d, 1month etc), and Natty (try examples http://natty.joestelmach.com/try.jsp#)
     * @param base_date - for relative date, locks the date to this origin
     * @return the machine readable date, or an error
     */
    public static Validation<String, Date> getForwardSchedule(final String human_readable_date,
            Optional<Date> base_date) {
        final Date adjusted_date = base_date.orElse(new Date());
        return _adjustments.stream()
                .map(adjust -> Date.from(adjusted_date.toInstant().plus(adjust._1(), adjust._2()))) // (adjust the date by the increasing adjustment)
                .map(adjusted -> getSchedule(human_readable_date, Optional.of(adjusted)))
                .filter(parsed -> parsed.isSuccess())
                .filter(parsed -> parsed.success().getTime() >= adjusted_date.getTime()).findFirst()
                .orElse(Validation
                        .fail(ErrorUtils.get(ErrorUtils.INVALID_DATETIME_FORMAT_PAST, human_readable_date)));
    }

    private static Pattern date_parser = Pattern.compile("^\\s*([0-9]+)\\s*([a-z][a-rt-z]*)s?\\s*$",
            Pattern.CASE_INSENSITIVE);
    private static final List<Tuple2<Long, TemporalUnit>> _adjustments = Arrays.asList(
            Tuples._2T(0L, ChronoUnit.HOURS), Tuples._2T(1L, ChronoUnit.HOURS), Tuples._2T(1L, ChronoUnit.DAYS),
            Tuples._2T(7L, ChronoUnit.DAYS), Tuples._2T(30L, ChronoUnit.DAYS));

}