org.thevortex.lighting.jinks.robot.Recurrence.java Source code

Java tutorial

Introduction

Here is the source code for org.thevortex.lighting.jinks.robot.Recurrence.java

Source

/*
 * Copyright (c) 2014 by the author(s).
 *
 * 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.thevortex.lighting.jinks.robot;

import java.io.IOException;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.TextStyle;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAccessor;
import java.util.Collections;
import java.util.Objects;
import java.util.SortedSet;
import java.util.TreeSet;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

/**
 * How an event may recur. Assumes the start of a week is the same as the {@link java.time} classes, which is MONDAY.
 *
 * @author E. A. Graham Jr.
 */
@JsonDeserialize(using = Recurrence.RecurrenceDeserializer.class)
@JsonSerialize(using = Recurrence.RecurrenceSerializer.class)
public class Recurrence {
    /**
     * Format for start/end times with useless timezone and date information.
     */
    public static final DateTimeFormatter ICAL_DT = new DateTimeFormatterBuilder().appendLiteral("TZID=")
            .parseLenient().appendZoneText(TextStyle.FULL).appendLiteral(':').appendValue(ChronoField.YEAR, 4)
            .appendValue(ChronoField.MONTH_OF_YEAR, 2).appendValue(ChronoField.DAY_OF_MONTH, 2).appendLiteral('T')
            .appendValue(ChronoField.HOUR_OF_DAY, 2).appendValue(ChronoField.MINUTE_OF_HOUR, 2)
            .appendValue(ChronoField.SECOND_OF_MINUTE, 2).toFormatter();
    public static final String DTSTART = "DTSTART;";
    public static final String DTEND = "DTEND;";
    public static final String FREQ = "FREQ=";
    public static final String RRULE = "RRULE:";
    public static final String BYDAY = "BYDAY=";

    protected LocalTime startTime;
    protected Duration duration;
    protected Frequency frequency;
    protected SortedSet<DayOfWeek> days = Collections.emptySortedSet();

    /**
     * When this starts.
     */
    public LocalTime getStartTime() {
        return startTime;
    }

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

    /**
     * How long it lasts if it's not just a start.
     */
    public Duration getDuration() {
        return duration;
    }

    public void setDuration(Duration duration) {
        this.duration = duration;
    }

    /**
     * How often.
     */
    public Frequency getFrequency() {
        return frequency;
    }

    public void setFrequency(Frequency frequency) {
        this.frequency = frequency;
    }

    /**
     * If WEEKLY, which DOW
     */
    public SortedSet<DayOfWeek> getDays() {
        return days;
    }

    public void setDays(SortedSet<DayOfWeek> days) {
        this.days = days;
    }

    public static enum Frequency {
        DAILY, WEEKLY
    }

    /**
     * Get the next occurrence from a time.
     *
     * @param fromWhen when
     * @return the next occurrence or {@code null} if there is no more
     */
    public LocalDateTime nextOccurrence(TemporalAccessor fromWhen) {
        LocalDateTime from = LocalDateTime.from(fromWhen);

        // if it's not today, try the next day
        if (frequency == Frequency.WEEKLY && !days.contains(from.getDayOfWeek())) {
            return nextOccurrence(from.plusDays(1).truncatedTo(ChronoUnit.DAYS));
        }

        // if we've already started, it's too late - next day
        if (from.toLocalTime().isAfter(startTime)) {
            return nextOccurrence(from.plusDays(1).truncatedTo(ChronoUnit.DAYS));
        }

        // otherwise, we're on the right day, so just adjust the time
        return from.with(startTime).truncatedTo(ChronoUnit.MINUTES);
    }

    /**
     * Determine if the target is within the boundaries of this event.
     *
     * @param target the target time/date
     * @return {@code true} if the target is after the start time and within the duration; {@code false} if outside
     * the duration and/or there is no duration
     */
    public boolean within(TemporalAccessor target) {
        if (duration == null)
            return false;

        LocalTime lt = LocalTime.from(target);
        return duration != null && lt.isAfter(startTime)
                && (Duration.between(startTime, lt).compareTo(duration) < 0);
    }

    /**
     * @return the ICAL-formatted string
     */
    @Override
    public String toString() {
        StringBuilder formatted = new StringBuilder(DTSTART)
                .append(ZonedDateTime.now().with(startTime).truncatedTo(ChronoUnit.MINUTES).format(ICAL_DT));

        // if we have a duration, there's an end
        if (duration != null) {
            formatted.append('\n').append(DTEND).append(ZonedDateTime.now().with(startTime.plus(duration))
                    .truncatedTo(ChronoUnit.MINUTES).format(ICAL_DT));
        }

        // always a frequency
        formatted.append('\n').append(RRULE).append(FREQ).append(frequency);

        if (frequency == Frequency.WEEKLY) {
            // build the buffer of days
            StringBuilder dayBuilder = new StringBuilder();
            boolean notFirst = false;
            DayOfWeek lastDay = null;
            for (DayOfWeek day : days) {
                if (notFirst) {
                    dayBuilder.append(',');
                }
                notFirst = true;
                dayBuilder.append(day.name().substring(0, 2));
                lastDay = day;
            }
            String formattedDays = dayBuilder.toString();

            // if SUNDAY is at the end, move it to the front
            if (lastDay == DayOfWeek.SUNDAY) {
                formattedDays = "SU," + formattedDays.substring(0, formattedDays.lastIndexOf(","));
            }
            // spit it out
            formatted.append(';').append(BYDAY).append(formattedDays);
        }
        return formatted.toString();
    }

    @Override
    public boolean equals(Object o) {
        if (o == null)
            return false;
        if (this == o)
            return true;

        if (o instanceof Recurrence) {
            Recurrence that = (Recurrence) o;

            return Objects.equals(startTime, that.startTime) && Objects.equals(duration, that.duration)
                    && Objects.equals(frequency, that.frequency) && Objects.equals(days, that.days);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hash(startTime, duration, frequency, days);
    }

    //=================================================================================================================

    /**
     * Parse from a partial ICAL string.
     *
     * @param recurrenceString the string
     * @return a thing
     */
    public static Recurrence parse(String recurrenceString) {
        if (recurrenceString == null)
            return null;

        Recurrence result = new Recurrence();

        // each major part will be on a separate line
        String[] parts = recurrenceString.split("\\n");

        // first one MUST be a "DTSTART;" or we're already fucked
        String working = checkAndStrip(DTSTART, parts[0]);
        result.startTime = LocalTime.parse(working, ICAL_DT);

        // if there are three parts, the second must be an end time
        if (parts.length == 3) {
            working = checkAndStrip(DTEND, parts[1]);
            LocalTime endsAt = LocalTime.parse(working, ICAL_DT);
            result.duration = Duration.between(result.startTime, endsAt);
        }

        // we always have a RRULE: first part is FREQ and if it's weekly, followed by BYDAY
        working = checkAndStrip(RRULE, parts[parts.length - 1]);
        parts = working.split(";");

        // the type is first
        working = checkAndStrip(FREQ, parts[0]);
        result.frequency = Frequency.valueOf(working);

        // if it's weekly, there's more
        if (result.frequency == Frequency.WEEKLY) {
            SortedSet<DayOfWeek> list = new TreeSet<>();

            working = checkAndStrip(BYDAY, parts[1]);
            for (String dayValue : working.split(",")) {
                for (DayOfWeek dow : DayOfWeek.values()) {
                    if (dow.name().startsWith(dayValue)) {
                        list.add(dow);
                        break;
                    }
                }
            }
            result.days = list;
        }

        return result;
    }

    protected static String checkAndStrip(String token, String victim) {
        if (!victim.startsWith(token)) {
            throw new IllegalArgumentException(
                    String.format("Expected token '%s' was not found in [%s]", token, victim));
        }
        return victim.substring(token.length());
    }

    // ================================================================================================================

    public static class RecurrenceSerializer extends JsonSerializer<Recurrence> {
        @Override
        public void serialize(Recurrence value, JsonGenerator jgen, SerializerProvider provider)
                throws IOException {
            String text = value.toString();
            // TODO not ready to write yet - need unit tests
            throw new UnsupportedOperationException(text);
            //            jgen.writeString(text);
        }
    }

    public static class RecurrenceDeserializer extends JsonDeserializer<Recurrence> {
        @Override
        public Recurrence deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
            return Recurrence.parse(jp.getValueAsString());
        }
    }
}