Skip to content

Commit

Permalink
Use chronology to define allowable range of recurrence values
Browse files Browse the repository at this point in the history
  • Loading branch information
benfortuna committed Jul 6, 2021
1 parent 0cbc093 commit 4a90a53
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 35 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Expand Up @@ -48,6 +48,9 @@ dependencies {
// optional timezone caching..
implementation 'javax.cache:cache-api:1.1.1', optional

// optional support for non-Gregorian chronologies..
implementation "org.threeten:threeten-extra:$threetenExtraVersion", optional

// optional groovy DSL for calendar builder..
implementation "org.codehaus.groovy:groovy:$groovyVersion", optional
implementation "org.codehaus.groovy:groovy-dateutil:$groovyVersion", optional
Expand Down
67 changes: 53 additions & 14 deletions src/main/java/net/fortuna/ical4j/model/NumberList.java
Expand Up @@ -34,8 +34,8 @@
import net.fortuna.ical4j.util.Numbers;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.StringTokenizer;
import java.time.temporal.ValueRange;
import java.util.*;
import java.util.stream.Collectors;

/**
Expand All @@ -49,45 +49,68 @@ public class NumberList extends ArrayList<Integer> implements Serializable {

private static final long serialVersionUID = -1667481795613729889L;

private final int minValue;

private final int maxValue;
private final ValueRange valueRange;

private final boolean allowsNegativeValues;

/**
* Default constructor.
*/
public NumberList() {
this(Integer.MIN_VALUE, Integer.MAX_VALUE, true);
this(ValueRange.of(Integer.MIN_VALUE, Integer.MAX_VALUE), true);
}

/**
* Construct a number list restricted by the specified {@link ValueRange}.
* @param valueRange a range defining the lower and upper bounds of allowed values
* @param allowsNegativeValues allow negative values, where abs(value) is within the specified range
*/
public NumberList(ValueRange valueRange, boolean allowsNegativeValues) {
this.valueRange = valueRange;
this.allowsNegativeValues = allowsNegativeValues;
}

/**
* Constructor with limits.
* @param minValue the minimum allowable value
* @param maxValue the maximum allowable value
* @param allowsNegativeValues indicates whether negative values are allowed
*
* @deprecated use {@link NumberList#NumberList(ValueRange, boolean)}
*/
@Deprecated
public NumberList(int minValue, int maxValue, boolean allowsNegativeValues) {
this.minValue = minValue;
this.maxValue = maxValue;
this.allowsNegativeValues = allowsNegativeValues;
this(ValueRange.of(minValue, maxValue), allowsNegativeValues);
}

/**
* Constructor.
* @param aString a string representation of a number list
*/
public NumberList(final String aString) {
this(aString, Integer.MIN_VALUE, Integer.MAX_VALUE, true);
this(aString, ValueRange.of(Integer.MIN_VALUE, Integer.MAX_VALUE), true);
}


/**
* Construct a number list restricted by the specified {@link ValueRange}.
* @param aString a string representation of a list of values
* @param valueRange a range defining the lower and upper bounds of allowed values
* @param allowsNegativeValues allow negative values, where abs(value) is within the specified range
*/
public NumberList(final String aString, ValueRange valueRange, boolean allowsNegativeValues) {
this(valueRange, allowsNegativeValues);
addAll(Arrays.stream(aString.split(",")).map(Numbers::parseInt).collect(Collectors.toList()));
}

/**
* @param aString a string representation of a number list
* @param minValue the minimum allowable value
* @param maxValue the maximum allowable value
* @param allowsNegativeValues indicates whether negative values are allowed
*
* @deprecated use {@link NumberList#NumberList(String, ValueRange, boolean)}
*/
@Deprecated
public NumberList(final String aString, int minValue, int maxValue, boolean allowsNegativeValues) {
this(minValue, maxValue, allowsNegativeValues);
final StringTokenizer t = new StringTokenizer(aString, ",");
Expand All @@ -110,13 +133,29 @@ public final boolean add(final Integer aNumber) {
}
abs = Math.abs(abs);
}
if (abs < minValue || abs > maxValue) {
throw new IllegalArgumentException(
"Value not in range [" + minValue + ".." + maxValue + "]: " + aNumber);
if (!valueRange.isValidIntValue(abs)) {
throw new IllegalArgumentException("Value not in range [" + valueRange + "]: " + aNumber);
}
return super.add(aNumber);
}

@Override
public boolean addAll(Collection<? extends Integer> c) {
Optional<? extends Integer> negativeValue = c.stream().filter(v -> (v >> 31 | -v >>> 31) < 0)
.findFirst();
if (!allowsNegativeValues && negativeValue.isPresent()) {
throw new IllegalArgumentException("Negative value not allowed: " + negativeValue.get());
}

Optional<? extends Integer> invalidValue = c.stream().filter(v -> !valueRange.isValidValue(Math.abs(v)))
.findFirst();
if (invalidValue.isPresent()) {
throw new IllegalArgumentException(
"Value not in range [" + valueRange + "]: " + invalidValue);
}
return super.addAll(c);
}

/**
* {@inheritDoc}
*/
Expand Down
72 changes: 51 additions & 21 deletions src/main/java/net/fortuna/ical4j/model/Recur.java
Expand Up @@ -43,7 +43,8 @@
import java.io.IOException;
import java.io.Serializable;
import java.text.ParseException;
import java.time.temporal.ValueRange;
import java.time.chrono.Chronology;
import java.time.temporal.ChronoField;
import java.util.Calendar;
import java.util.*;

Expand Down Expand Up @@ -95,6 +96,30 @@ public enum Frequency {
SECONDLY, MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY;
}

public enum RScale {

JAPANESE("Japanese"),
BUDDHIST("ThaiBuddhist"),
ROC("Minguo"),
ISLAMIC("islamic"),
ISO8601("ISO"),

CHINESE("ISO"),
ETHIOPIC("Ethiopic"),
HEBREW("ISO"),
GREGORIAN("ISO");

private final String chronology;

RScale(String chronology) {
this.chronology = chronology;
}

public String getChronology() {
return chronology;
}
}

public enum Skip {
OMIT, BACKWARD, FORWARD;
}
Expand Down Expand Up @@ -170,7 +195,7 @@ public enum Skip {

private Date until;

private String rscale;
private RScale rscale;

private Integer count;

Expand Down Expand Up @@ -225,6 +250,7 @@ public Recur(final String aValue) throws ParseException {
// default week start is Monday per RFC5545
calendarWeekStartDay = Calendar.MONDAY;

Chronology chronology = Chronology.ofLocale(Locale.getDefault());
Iterator<String> tokens = Arrays.asList(aValue.split("[;=]")).iterator();
while (tokens.hasNext()) {
final String token = tokens.next();
Expand All @@ -233,7 +259,8 @@ public Recur(final String aValue) throws ParseException {
} else if (SKIP.equals(token)) {
skip = Skip.valueOf(nextToken(tokens, token));
} else if (RSCALE.equals(token)) {
rscale = nextToken(tokens, token);
rscale = RScale.valueOf(nextToken(tokens, token));
chronology = Chronology.of(rscale.getChronology());
} else if (UNTIL.equals(token)) {
final String untilString = nextToken(tokens, token);
if (untilString != null && untilString.contains("T")) {
Expand All @@ -248,23 +275,23 @@ public Recur(final String aValue) throws ParseException {
} else if (INTERVAL.equals(token)) {
interval = Integer.parseInt(nextToken(tokens, token));
} else if (BYSECOND.equals(token)) {
secondList = new NumberList(nextToken(tokens, token), 0, 59, false);
secondList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.SECOND_OF_MINUTE), false);
} else if (BYMINUTE.equals(token)) {
minuteList = new NumberList(nextToken(tokens, token), 0, 59, false);
minuteList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.MINUTE_OF_HOUR), false);
} else if (BYHOUR.equals(token)) {
hourList = new NumberList(nextToken(tokens, token), 0, 23, false);
hourList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.HOUR_OF_DAY), false);
} else if (BYDAY.equals(token)) {
dayList = new WeekDayList(nextToken(tokens, token));
} else if (BYMONTHDAY.equals(token)) {
monthDayList = new NumberList(nextToken(tokens, token), 1, 31, true);
monthDayList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.DAY_OF_MONTH), true);
} else if (BYYEARDAY.equals(token)) {
yearDayList = new NumberList(nextToken(tokens, token), 1, 366, true);
yearDayList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.DAY_OF_YEAR), true);
} else if (BYWEEKNO.equals(token)) {
weekNoList = new NumberList(nextToken(tokens, token), 1, 53, true);
weekNoList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.ALIGNED_WEEK_OF_YEAR), true);
} else if (BYMONTH.equals(token)) {
monthList = new MonthList(nextToken(tokens, token), ValueRange.of(1, 12, 13));
monthList = new MonthList(nextToken(tokens, token), chronology.range(ChronoField.MONTH_OF_YEAR));
} else if (BYSETPOS.equals(token)) {
setPosList = new NumberList(nextToken(tokens, token), 1, 366, true);
setPosList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.DAY_OF_YEAR), true);
} else if (WKST.equals(token)) {
weekStartDay = WeekDay.Day.valueOf(nextToken(tokens, token));
calendarWeekStartDay = WeekDay.getCalendarDay(WeekDay.getWeekDay(weekStartDay));
Expand Down Expand Up @@ -336,41 +363,43 @@ public Recur(final Frequency frequency, final int count) {

private void initTransformers() {
transformers = new HashMap<>();
Chronology chronology = rscale != null ? Chronology.of(rscale.getChronology())
: Chronology.ofLocale(Locale.getDefault());
if (secondList != null) {
transformers.put(BYSECOND, new BySecondRule(secondList, frequency, Optional.ofNullable(weekStartDay)));
} else {
secondList = new NumberList(0, 59, false);
secondList = new NumberList(chronology.range(ChronoField.SECOND_OF_MINUTE), false);
}
if (minuteList != null) {
transformers.put(BYMINUTE, new ByMinuteRule(minuteList, frequency, Optional.ofNullable(weekStartDay)));
} else {
minuteList = new NumberList(0, 59, false);
minuteList = new NumberList(chronology.range(ChronoField.MINUTE_OF_HOUR), false);
}
if (hourList != null) {
transformers.put(BYHOUR, new ByHourRule(hourList, frequency, Optional.ofNullable(weekStartDay)));
} else {
hourList = new NumberList(0, 23, false);
hourList = new NumberList(chronology.range(ChronoField.HOUR_OF_DAY), false);
}
if (monthDayList != null) {
transformers.put(BYMONTHDAY, new ByMonthDayRule(monthDayList, frequency, Optional.ofNullable(weekStartDay)));
} else {
monthDayList = new NumberList(1, 31, true);
monthDayList = new NumberList(chronology.range(ChronoField.DAY_OF_MONTH), true);
}
if (yearDayList != null) {
transformers.put(BYYEARDAY, new ByYearDayRule(yearDayList, frequency, Optional.ofNullable(weekStartDay)));
} else {
yearDayList = new NumberList(1, 366, true);
yearDayList = new NumberList(chronology.range(ChronoField.DAY_OF_YEAR), true);
}
if (weekNoList != null) {
transformers.put(BYWEEKNO, new ByWeekNoRule(weekNoList, frequency, Optional.ofNullable(weekStartDay)));
} else {
weekNoList = new NumberList(1, 53, true);
weekNoList = new NumberList(chronology.range(ChronoField.ALIGNED_WEEK_OF_YEAR), true);
}
if (monthList != null) {
transformers.put(BYMONTH, new ByMonthRule(monthList, frequency,
Optional.ofNullable(weekStartDay)));
} else {
monthList = new MonthList(ValueRange.of(1, 12, 13));
monthList = new MonthList(chronology.range(ChronoField.MONTH_OF_YEAR));
}
if (dayList != null) {
transformers.put(BYDAY, new ByDayRule(dayList, deriveFilterType(), Optional.ofNullable(weekStartDay)));
Expand All @@ -380,7 +409,7 @@ private void initTransformers() {
if (setPosList != null) {
transformers.put(BYSETPOS, new BySetPosRule(setPosList));
} else {
setPosList = new NumberList(1, 366, true);
setPosList = new NumberList(chronology.range(ChronoField.DAY_OF_YEAR), true);
}
}

Expand Down Expand Up @@ -1106,7 +1135,7 @@ public static class Builder {

private Date until;

private String rscale;
private RScale rscale;

private Integer count;

Expand Down Expand Up @@ -1147,7 +1176,7 @@ public Builder until(Date until) {
return this;
}

public Builder rscale(String rscale) {
public Builder rscale(RScale rscale) {
this.rscale = rscale;
return this;
}
Expand Down Expand Up @@ -1215,6 +1244,7 @@ public Builder weekStartDay(WeekDay.Day weekStartDay) {
public Recur build() {
Recur recur = new Recur();
recur.frequency = frequency;
recur.rscale = rscale;
recur.skip = skip;
recur.until = until;
recur.count = count;
Expand Down

0 comments on commit 4a90a53

Please sign in to comment.