All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jfxtras.icalendarfx.properties.component.recurrence.rrule.byxxx.ByDay Maven / Gradle / Ivy

There is a newer version: 17-r1
Show newest version
package jfxtras.icalendarfx.properties.component.recurrence.rrule.byxxx;

import static java.time.temporal.ChronoField.DAY_OF_WEEK;
import static java.time.temporal.ChronoUnit.DAYS;

import java.time.DayOfWeek;
import java.time.Month;
import java.time.Year;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.TemporalField;
import java.time.temporal.WeekFields;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import jfxtras.icalendarfx.properties.component.recurrence.rrule.RRuleElement;
import jfxtras.icalendarfx.properties.component.recurrence.rrule.RecurrenceRuleValue;
import jfxtras.icalendarfx.properties.component.recurrence.rrule.WeekStart;
import jfxtras.icalendarfx.properties.component.recurrence.rrule.byxxx.ByDay;
import jfxtras.icalendarfx.properties.component.recurrence.rrule.byxxx.ByRuleAbstract;
import jfxtras.icalendarfx.properties.component.recurrence.rrule.byxxx.ByDay.ByDayPair;
import jfxtras.icalendarfx.utilities.DateTimeUtilities;

/** BYDAY from RFC 5545, iCalendar 3.3.10, page 40
 * 
 * The BYDAY rule part specifies a COMMA-separated list of days of
 * the week; SU indicates Sunday; MO indicates Monday; TU indicates
 * Tuesday; WE indicates Wednesday; TH indicates Thursday; FR
 * indicates Friday; and SA indicates Saturday.
 *
 * Each BYDAY value can also be preceded by a positive (+n) or
 * negative (-n) integer.  If present, this indicates the nth
 * occurrence of a specific day within the MONTHLY or YEARLY "RRULE".
 *
 * For example, within a MONTHLY rule, +1MO (or simply 1MO)
 * represents the first Monday within the month, whereas -1MO
 * represents the last Monday of the month.  The numeric value in a
 * BYDAY rule part with the FREQ rule part set to YEARLY corresponds
 * to an offset within the month when the BYMONTH rule part is
 * present, and corresponds to an offset within the year when the
 * BYWEEKNO or BYMONTH rule parts are present.  If an integer
 * modifier is not present, it means all days of this type within the
 * specified frequency.  For example, within a MONTHLY rule, MO
 * represents all Mondays within the month.  The BYDAY rule part MUST
 * NOT be specified with a numeric value when the FREQ rule part is
 * not set to MONTHLY or YEARLY.  Furthermore, the BYDAY rule part
 * MUST NOT be specified with a numeric value with the FREQ rule part
 * set to YEARLY when the BYWEEKNO rule part is specified.
 * 
 * Element value is a ByDayPair that contains a DayOfWeek and an optional ordinal int.
 * if the ordinal int is 0 then it is ignored and all values matching the DayOfWeek are included.
 * 
 * */
public class ByDay extends ByRuleAbstract
{   
    private final static int MIN_DAYS_IN_WEEK = 4;
    /** Start of week - default start of week is Monday */
    private DayOfWeek getWeekStart()
    {
    	if (getParent() != null)
    	{
    		WeekStart weekStart = ((RecurrenceRuleValue) getParent()).getWeekStart();
			return (weekStart == null) ? WeekStart.DEFAULT_WEEK_START : weekStart.getValue();
    	} else
    	{
    		return WeekStart.DEFAULT_WEEK_START;
    	}
	}
    
    //CONSTRUCTORS
    /** Parse iCalendar compliant list of days of the week.  For example 1MO,2TU,4SA
     */
    public ByDay()
    {
        super();
    }
    
    public ByDay(ByDayPair... byDayPairs)
    {
        this();
        setValue(byDayPairs);
    }
    
    public ByDay(ByDay source)
    {
        super(source);
    }

    /** Constructor that uses {@link DayofWeek} values without a preceding integer.  All days of the 
     * provided types are included within the specified frequency */
    public ByDay(DayOfWeek... daysOfWeek)
    {
        this(Arrays.asList(daysOfWeek));
    }

    /** Constructor that uses {@link DayofWeek} Collection.  No ordinals are allowed. */
    public ByDay(Collection daysOfWeek)
    {
        this();
        ByDayPair[] dayArray = daysOfWeek.stream()
            .map(d -> new ByDayPair(d,0))
            .toArray(size -> new ByDayPair[size]);
        setValue(dayArray);
    }

    
    
    /** Checks if byDayPairs has ordinal values.  If so returns true, otherwise false */
    public boolean hasOrdinals()
    {
        return getValue()
                .stream()
                .filter(p -> (p.ordinal != 0))
                .findAny()
                .isPresent();
    }
    
    /** add individual {@link DayofWeek}, without ordinal value, to BYDAY rule
     * 
     * @param dayOfWeek {@link DayofWeek} to add, without ordinal
     * @return true if added, false if DayOfWeek already present
     */
    public boolean addDayOfWeek(DayOfWeek dayOfWeek)
    {
        boolean isPresent = getValue()
            .stream()
            .map(a -> a.dayOfWeek)
            .filter(d -> d == dayOfWeek)
            .findAny()
            .isPresent();
        if (! isPresent)
        {
            getValue().add(new ByDayPair(dayOfWeek, 0));
            return true;
        }
        return false;
    }

    /** remove individual DayofWeek from BYDAY rule
     * 
     * @param dayOfWeek {@link DayofWeek} to remove
     * @return true if removed, false if not present
     */
    public boolean removeDayOfWeek(DayOfWeek dayOfWeek)
    {
        ByDayPair p = getValue().stream()
                .filter(v -> v.dayOfWeek == dayOfWeek)
                .findAny()
                .orElse(null);
        if (p != null)
        {
            getValue().remove(p);
            return true;
        }
        return false;
    }
    
    /** Replace individual {@link DayofWeek} in BYDAY rule
     * If {@link ByDayPair} contains a non-zero ordinal, the replacement contains the same ordinal value
     * Note: a zero ordinal means include all matching {@link DayofWeek} values
     * 
     * @param original {@link DayofWeek} to remove
     * @param replacement {@link DayofWeek} to add
     * @return true if replaced, false if original is not present
     */
    public boolean replaceDayOfWeek(DayOfWeek originalDayOfWeek, DayOfWeek replacemenDayOfWeekt)
    {
        ByDayPair p = getValue().stream()
                .filter(v -> v.dayOfWeek == originalDayOfWeek)
                .findAny()
                .orElse(null);
        if (p != null)
        {
            int ordinal = p.getOrdinal();
            getValue().remove(p);
            getValue().add(new ByDayPair(replacemenDayOfWeekt, ordinal));
            return true;
        }
        return false;
    }
    
    /** Return a list of days of the week that don't have an ordinal (as every FRIDAY) */
    public List dayOfWeekWithoutOrdinalList()
    {
        return getValue().stream()
                     .filter(d -> d.ordinal == 0)
                     .map(d -> d.dayOfWeek)
                     .collect(Collectors.toList());
    }
    
    @Override
    public String toString()
    {
    	if (getValue() == null) return "";
        String days = getValue().stream()
                .map(d ->
                {
                    String day = d.dayOfWeek.toString().substring(0, 2); // + ",";
                    return (d.ordinal == 0) ? day : d.ordinal + day;
                })
                .collect(Collectors.joining(","));
        return RRuleElement.BY_DAY + "=" + days; //.substring(0, days.length()-1); // remove last comma
    }
    
    @Override
    public Stream streamRecurrences(Stream inStream, ChronoUnit chronoUnit, Temporal dateTimeStart)
    {
        /* TODO - according to iCalendar standard a ByDay rule doesn't need any specified days - should use day from DTSTART,
         * this is not implemented yet.  When implemented this line should be removed. */
        switch (chronoUnit)
        {
        case HOURS:
        case MINUTES:
        case SECONDS:
        case DAYS:
        {
            boolean isValid = getValue().stream().allMatch(v -> v.getOrdinal() == 0);
            if (! isValid)
            {
                throw new IllegalArgumentException("Numberic ordinal day values can't be set for FREQ as" + chronoUnit);
            }
            return inStream.filter(t ->
            { // filter out all but qualifying days
                DayOfWeek myDayOfWeek = DayOfWeek.from(t);
                for (ByDayPair byDayPair : getValue())
                {
                    if (byDayPair.dayOfWeek == myDayOfWeek) return true;
                }
                return false;
            });
        }
        case WEEKS:
        {
            boolean isValid = getValue().stream().allMatch(v -> v.getOrdinal() == 0);
            if (! isValid)
            {
                throw new IllegalArgumentException("Numberic ordinal day values can't be set for FREQ as " + chronoUnit);
            }
            WeekFields weekFields = WeekFields.of(getWeekStart(), MIN_DAYS_IN_WEEK);
            TemporalField dayOfWeekField = weekFields.dayOfWeek();
            return inStream.flatMap(t -> 
            { // Expand to be byDayPairs days in current week
                List dates = new ArrayList<>();
                for (ByDayPair byDayPair : getValue())
                {
                    int defaultFirstDayOfWeekValue = DayOfWeek.MONDAY.getValue();
                    int myFirstDayOfWeekValue = weekFields.getFirstDayOfWeek().getValue();
                    int dayOfWeekAdjustment = defaultFirstDayOfWeekValue - myFirstDayOfWeekValue + DayOfWeek.values().length;
                    int dayOfWeekValue = byDayPair.dayOfWeek.getValue() + dayOfWeekAdjustment;
                    dayOfWeekValue = (dayOfWeekValue > 7) ? dayOfWeekValue-7 : dayOfWeekValue;
                    Temporal newTemporal = t.with(dayOfWeekField, dayOfWeekValue);
                    dates.add(newTemporal);
                }
                if (getValue().size() > 1) Collections.sort(dates, DateTimeUtilities.TEMPORAL_COMPARATOR);
                return dates.stream();
            });
        }
        case MONTHS:
            return inStream.flatMap(date -> 
            {
                List dates = new ArrayList<>();
                for (ByDayPair byDayPair : getValue())
                {
                    if (byDayPair.ordinal == 0)
                    { // add every matching day of week in month
                        Month myMonth = Month.from(date);
                        for (int weekNum=1; weekNum<=5; weekNum++)
                        {
                            Temporal newTemporal = date.with(TemporalAdjusters.dayOfWeekInMonth(weekNum, byDayPair.dayOfWeek));
                            if (Month.from(newTemporal) == myMonth)
                            {
                                dates.add(newTemporal);
                            }
                        }
                    } else
                    {
                        Month myMonth = Month.from(date);
                        Temporal newTemporal = date.with(TemporalAdjusters.dayOfWeekInMonth(byDayPair.ordinal, byDayPair.dayOfWeek));

                        if (Month.from(newTemporal) == myMonth)
                        {
                            dates.add(newTemporal);
                        }
                    }
                }
                if (getValue().size() > 1) Collections.sort(dates, DateTimeUtilities.TEMPORAL_COMPARATOR);
                return dates.stream();
            });
        case YEARS:
            return inStream.flatMap(date -> 
            {
                List dates = new ArrayList<>();
                for (ByDayPair byDayPair : getValue())
                {
                    if (byDayPair.ordinal == 0)
                    { // add every matching day of week in year
                        Temporal newDate = date
                                .with(TemporalAdjusters.firstDayOfYear())
                                .with(TemporalAdjusters.nextOrSame(byDayPair.dayOfWeek));
                        while (Year.from(newDate).equals(Year.from(date)))
                        {
                            dates.add(newDate);
                            newDate = newDate.plus(1, ChronoUnit.WEEKS);
                        }
                    } else
                    { // if never any ordinal numbers then sort is not required
                        Temporal newDate = date.with(dayOfWeekInYear(byDayPair.ordinal, byDayPair.dayOfWeek));
                        dates.add(newDate);
                    }
                }
                if (getValue().size() > 1) Collections.sort(dates, DateTimeUtilities.TEMPORAL_COMPARATOR);
                return dates.stream();
            }); 
        default:
            throw new RuntimeException("Not implemented ChronoUnit: " + chronoUnit);
        }
    }

    /** Finds nth occurrence of a week in a year.
     * Based on TemporalAdjusters.dayOfWeekInMonth */
    private TemporalAdjuster dayOfWeekInYear(int ordinal, DayOfWeek dayOfWeek)
    {
        int dowValue = dayOfWeek.getValue();
        return (temporal) -> {
            Temporal temp = (ordinal > 0) ? temporal.with(TemporalAdjusters.firstDayOfYear()) :
                temporal.plus(1, ChronoUnit.YEARS).with(TemporalAdjusters.firstDayOfYear());
            int curDow = temp.get(DAY_OF_WEEK);
            int dowDiff = (dowValue - curDow + 7) % 7;
            dowDiff = (ordinal > 0) ? dowDiff + (ordinal - 1) * 7 : dowDiff + (ordinal) * 7;
            return temp.plus(dowDiff, DAYS);
        };
    }
    
    /**
     * Contains both the day of the week and an optional positive or negative integer (ordinal).
     * If the integer is present it represents the nth occurrence of a specific day within the 
     * MONTHLY or YEARLY frequency rules.  For example, with a MONTHLY rule 1MO indicates the
     * first Monday of the month.
     * If ordinal is 0 then all the matching days are included within the specified frequency rule.
     */
    public static class ByDayPair
    {
        private DayOfWeek dayOfWeek;
        public DayOfWeek getDayOfWeek() { return dayOfWeek; }
        public void setDayOfWeek(DayOfWeek dayOfWeek) { this.dayOfWeek = dayOfWeek; }
        public ByDayPair withDayOfWeek(DayOfWeek dayOfWeek) { setDayOfWeek(dayOfWeek); return this; }
        
        private int ordinal = 0;
        public int getOrdinal() { return ordinal; }
        public void setOrdinal(int ordinal) { this.ordinal = ordinal; }
        public ByDayPair withOrdinal(int ordinal) { setOrdinal(ordinal); return this; }

        public ByDayPair(DayOfWeek dayOfWeek, int ordinal)
        {
            this.dayOfWeek = dayOfWeek;
            this.ordinal = ordinal;
        }
        
        public ByDayPair() { }

        @Override
        public boolean equals(Object obj)
        {
            if (obj == this) return true;
            if((obj == null) || (obj.getClass() != getClass())) {
                return false;
            }
            ByDayPair testObj = (ByDayPair) obj;
            return (dayOfWeek == testObj.dayOfWeek)
                    && (ordinal == testObj.ordinal);
        }
        
        @Override
        public int hashCode()
        {
            int hash = 7;
            hash = (31 * hash) + dayOfWeek.hashCode();
            hash = (31 * hash) + ordinal;
            return hash;
        }
        
        @Override
        public String toString()
        {
            return super.toString() + ", " + getDayOfWeek() + ", " + getOrdinal();
        }        
    }
    
    @Override
    protected List parseContent(String dayPairs)
    {
    	String valueString = extractValue(dayPairs);
        List dayPairsList = new ArrayList();
        Pattern p = Pattern.compile("(-?[0-9]+)?([A-Z]{2})");
        Matcher m = p.matcher(valueString);
        while (m.find())
        {
            String token = m.group();
            if (token.matches("^(-?[0-9]+.*)")) // start with ordinal number
            {
                Matcher m2 = p.matcher(token);
                if (m2.find())
                {
//                    DayOfWeek dayOfWeek = ICalendarDayOfWeek.valueOf(m2.group(2)).getDayOfWeek();
                    DayOfWeek dayOfWeek = DateTimeUtilities.dayOfWeekFromAbbreviation(m2.group(2));
                    int ordinal = Integer.parseInt(m2.group(1));
                    dayPairsList.add(new ByDayPair(dayOfWeek, ordinal));
                }
            } else
            { // has no ordinal number
                DayOfWeek dayOfWeek = DateTimeUtilities.dayOfWeekFromAbbreviation(token);
                dayPairsList.add(new ByDayPair(dayOfWeek, 0));
            }
        }
        setValue(dayPairsList);
//        return errors()  // Too slow - is it OK to ignore?
//        	.stream()
//        	.map(s -> new Message(this, s, MessageEffect.MESSAGE_ONLY))
//        	.collect(Collectors.toList());
        return Collections.EMPTY_LIST;
    }

    public static ByDay parse(String content)
    {
    	return ByDay.parse(new ByDay(), content);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy