jfxtras.icalendarfx.properties.component.recurrence.rrule.byxxx.ByDay Maven / Gradle / Ivy
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