From 0ad973eaf8a2b6faad99b2f4d3dc68a11d447d6b Mon Sep 17 00:00:00 2001 From: Mike Angstadt Date: Sun, 29 Oct 2023 12:27:36 -0400 Subject: [PATCH] Reduce cognitive complexity Move vCal 1.0 RRULE parsing/writing code into separate classes. Parsing code has been significantly refactored --- .../scribe/property/RecurrenceParserV1.java | 389 ++++++++++++++++ .../property/RecurrencePropertyScribe.java | 420 ++---------------- .../scribe/property/RecurrenceWriterV1.java | 129 ++++++ 3 files changed, 544 insertions(+), 394 deletions(-) create mode 100644 src/main/java/biweekly/io/scribe/property/RecurrenceParserV1.java create mode 100644 src/main/java/biweekly/io/scribe/property/RecurrenceWriterV1.java diff --git a/src/main/java/biweekly/io/scribe/property/RecurrenceParserV1.java b/src/main/java/biweekly/io/scribe/property/RecurrenceParserV1.java new file mode 100644 index 00000000..1c5fcef1 --- /dev/null +++ b/src/main/java/biweekly/io/scribe/property/RecurrenceParserV1.java @@ -0,0 +1,389 @@ +package biweekly.io.scribe.property; + +import static biweekly.io.scribe.property.ICalPropertyScribe.date; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import biweekly.io.CannotParseException; +import biweekly.io.ParseContext; +import biweekly.util.DayOfWeek; +import biweekly.util.Frequency; +import biweekly.util.Recurrence; + +/** + * Parses iCal 1.0 RRULE values. + * @author Michael Angstadt + */ +class RecurrenceParserV1 { + private final ParseContext context; + + public RecurrenceParserV1(ParseContext context) { + this.context = context; + } + + /** + * Parses an iCal 1.0 RRULE value. + * @param value the RRULE value + * @return the parsed recurrence + * @throws CannotParseException if there is a problem parsing the value + */ + public Recurrence parse(String value) { + Recurrence.Builder builder = new Recurrence.Builder((Frequency) null); + LinkedList tokens = splitTokens(value); + + String frequencyStr = parseFrequencyAndInterval(tokens, builder); + parseCountAndUntil(tokens, builder); + + TokenHandler tokenHandler = getTokenHandler(frequencyStr); + builder.frequency(tokenHandler.frequency()); + for (String token : tokens) { + //TODO Don't know how to handle the "$" symbol, ignore it. + if (token.endsWith("$")) { + context.addWarning(36, token); + token = removeLastChar(token); + } + + tokenHandler.processToken(token, builder); + } + tokenHandler.noMoreTokens(builder); + + return builder.build(); + } + + private String removeLastChar(String s) { + return s.substring(0, s.length() - 1); + } + + private String parseFrequencyAndInterval(LinkedList tokens, Recurrence.Builder builder) { + String token = tokens.remove(0); + + Pattern p = Pattern.compile("^([A-Z]+)(\\d+)$"); + Matcher m = p.matcher(token); + if (!m.find()) { + throw new CannotParseException(40, token); + } + + builder.interval(integerValueOf(m.group(2))); + return m.group(1); + } + + private void parseCountAndUntil(LinkedList tokens, Recurrence.Builder builder) { + final int DEFAULT_COUNT = 2; + + if (tokens.isEmpty()) { + builder.count(DEFAULT_COUNT); + return; + } + + String lastToken = tokens.getLast(); + + //is the last token COUNT? + if (lastToken.startsWith("#")) { + String countStr = lastToken.substring(1); + Integer count = integerValueOf(countStr); + if (count == 0) { + //infinite + } else { + builder.count(count); + } + + tokens.removeLast(); + return; + } + + //is the last token UNTIL? + try { + builder.until(date(lastToken).parse()); + tokens.removeLast(); + } catch (IllegalArgumentException e) { + //last token is a regular value + builder.count(DEFAULT_COUNT); + } + } + + private TokenHandler getTokenHandler(String frequencyStr) { + if ("YD".equals(frequencyStr)) { + return new YDHandler(); + } else if ("YM".equals(frequencyStr)) { + return new YMHandler(); + } else if ("MD".equals(frequencyStr)) { + return new MDHandler(); + } else if ("MP".equals(frequencyStr)) { + return new MPHandler(); + } else if ("W".equals(frequencyStr)) { + return new WHandler(); + } else if ("D".equals(frequencyStr)) { + return new DHandler(); + } else if ("M".equals(frequencyStr)) { + return new MHandler(); + } + + throw new CannotParseException(41, frequencyStr); + } + + private LinkedList splitTokens(String value) { + String valueUpper = value.toUpperCase(); + String[] split = valueUpper.split("\\s+"); + return new LinkedList(Arrays.asList(split)); + } + + private interface TokenHandler { + Frequency frequency(); + + void processToken(String token, Recurrence.Builder builder); + + void noMoreTokens(Recurrence.Builder builder); + } + + private class YDHandler implements TokenHandler { + @Override + public Frequency frequency() { + return Frequency.YEARLY; + } + + @Override + public void processToken(String token, Recurrence.Builder builder) { + Integer dayOfYear = integerValueOf(token); + builder.byYearDay(dayOfYear); + } + + @Override + public void noMoreTokens(Recurrence.Builder builder) { + //empty + } + } + + private class YMHandler implements TokenHandler { + @Override + public Frequency frequency() { + return Frequency.YEARLY; + } + + @Override + public void processToken(String token, Recurrence.Builder builder) { + Integer month = integerValueOf(token); + builder.byMonth(month); + } + + @Override + public void noMoreTokens(Recurrence.Builder builder) { + //empty + } + } + + private class MDHandler implements TokenHandler { + @Override + public Frequency frequency() { + return Frequency.MONTHLY; + } + + @Override + public void processToken(String token, Recurrence.Builder builder) { + try { + Integer date = "LD".equals(token) ? -1 : parseVCalInt(token); + builder.byMonthDay(date); + } catch (NumberFormatException e) { + throw new CannotParseException(40, token); + } + } + + @Override + public void noMoreTokens(Recurrence.Builder builder) { + //empty + } + } + + private class MPHandler implements TokenHandler { + private final List nums = new ArrayList(); + private final List days = new ArrayList(); + private boolean readNum = false; + + @Override + public Frequency frequency() { + return Frequency.MONTHLY; + } + + @Override + public void processToken(String token, Recurrence.Builder builder) { + if (token.matches("\\d{4}")) { + readNum = false; + + Integer hour = integerValueOf(token.substring(0, 2)); + builder.byHour(hour); + + Integer minute = integerValueOf(token.substring(2, 4)); + builder.byMinute(minute); + + return; + } + + try { + Integer curNum = parseVCalInt(token); + + if (!readNum) { + //reset lists, new segment + for (Integer num : nums) { + for (DayOfWeek day : days) { + builder.byDay(num, day); + } + } + nums.clear(); + days.clear(); + + readNum = true; + } + + nums.add(curNum); + } catch (NumberFormatException e) { + readNum = false; + days.add(parseDay(token)); + } + } + + @Override + public void noMoreTokens(Recurrence.Builder builder) { + for (Integer num : nums) { + for (DayOfWeek day : days) { + builder.byDay(num, day); + } + } + } + } + + private class WHandler implements TokenHandler { + @Override + public Frequency frequency() { + return Frequency.WEEKLY; + } + + @Override + public void processToken(String token, Recurrence.Builder builder) { + DayOfWeek day = parseDay(token); + builder.byDay(day); + } + + @Override + public void noMoreTokens(Recurrence.Builder builder) { + //empty + } + } + + private class DHandler implements TokenHandler { + @Override + public Frequency frequency() { + return Frequency.DAILY; + } + + @Override + public void processToken(String token, Recurrence.Builder builder) { + Integer hour = integerValueOf(token.substring(0, 2)); + builder.byHour(hour); + + Integer minute = integerValueOf(token.substring(2, 4)); + builder.byMinute(minute); + } + + @Override + public void noMoreTokens(Recurrence.Builder builder) { + //empty + } + } + + private class MHandler implements TokenHandler { + @Override + public Frequency frequency() { + return Frequency.MINUTELY; + } + + @Override + public void processToken(String token, Recurrence.Builder builder) { + //TODO can this ever have values? + } + + @Override + public void noMoreTokens(Recurrence.Builder builder) { + //empty + } + } + + /** + * Same as {@link Integer#valueOf(String)}, but throws a + * {@link CannotParseException} when it fails. + * @param value the string to parse + * @return the parsed integer + * @throws CannotParseException if the string cannot be parsed + */ + private Integer integerValueOf(String value) { + try { + return Integer.valueOf(value); + } catch (NumberFormatException e) { + throw new CannotParseException(40, value); + } + } + + /** + * Parses an integer string, where the sign is at the end of the string + * instead of at the beginning. + * @param value the integer string (e.g. "5-") + * @return the value (e.g. -5) + * @throws NumberFormatException if the string cannot be parsed as an + * integer + */ + private int parseVCalInt(String value) { + int negate; + String num; + if (value.endsWith("+")) { + num = removeLastChar(value); + negate = 1; + } else if (value.endsWith("-")) { + num = removeLastChar(value); + negate = -1; + } else { + num = value; + negate = 1; + } + + return Integer.parseInt(num) * negate; + } + + private DayOfWeek parseDay(String value) { + DayOfWeek day = DayOfWeek.valueOfAbbr(value); + if (day == null) { + throw new CannotParseException(42, value); + } + + return day; + } + + /** + * iCal version 1.0 allows multiple RRULE values to be defined inside of the + * same property. This method extracts each RRULE value from the property + * value. + * @param value the property value + * @return the RRULE values + */ + public static List splitPropertyValue(String value) { + List values = new ArrayList(); + Pattern p = Pattern.compile("#\\d+|\\d{8}T\\d{6}Z?"); + Matcher m = p.matcher(value); + + int prevIndex = 0; + while (m.find()) { + int end = m.end(); + String subValue = value.substring(prevIndex, end).trim(); + values.add(subValue); + prevIndex = end; + } + String subValue = value.substring(prevIndex).trim(); + if (subValue.length() > 0) { + values.add(subValue); + } + + return values; + } +} diff --git a/src/main/java/biweekly/io/scribe/property/RecurrencePropertyScribe.java b/src/main/java/biweekly/io/scribe/property/RecurrencePropertyScribe.java index a27742a9..245ad729 100644 --- a/src/main/java/biweekly/io/scribe/property/RecurrencePropertyScribe.java +++ b/src/main/java/biweekly/io/scribe/property/RecurrencePropertyScribe.java @@ -1,7 +1,5 @@ package biweekly.io.scribe.property; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.regex.Matcher; @@ -108,91 +106,7 @@ protected String _writeText(T property, WriteContext context) { } private String writeTextV1(T property, WriteContext context) { - Recurrence recur = property.getValue(); - Frequency frequency = recur.getFrequency(); - if (frequency == null) { - return ""; - } - - StringBuilder sb = new StringBuilder(); - - Integer interval = recur.getInterval(); - if (interval == null) { - interval = 1; - } - - switch (frequency) { - case YEARLY: - if (!recur.getByMonth().isEmpty()) { - sb.append("YM").append(interval); - for (Integer month : recur.getByMonth()) { - sb.append(' ').append(month); - } - } else { - sb.append("YD").append(interval); - for (Integer day : recur.getByYearDay()) { - sb.append(' ').append(day); - } - } - break; - - case MONTHLY: - if (!recur.getByMonthDay().isEmpty()) { - sb.append("MD").append(interval); - for (Integer day : recur.getByMonthDay()) { - sb.append(' ').append(writeVCalInt(day)); - } - } else { - sb.append("MP").append(interval); - for (ByDay byDay : recur.getByDay()) { - DayOfWeek day = byDay.getDay(); - Integer prefix = byDay.getNum(); - if (prefix == null) { - prefix = 1; - } - - sb.append(' ').append(writeVCalInt(prefix)).append(' ').append(day.getAbbr()); - } - } - break; - - case WEEKLY: - sb.append("W").append(interval); - for (ByDay byDay : recur.getByDay()) { - sb.append(' ').append(byDay.getDay().getAbbr()); - } - break; - - case DAILY: - sb.append("D").append(interval); - break; - - case HOURLY: - sb.append("M").append(interval * 60); - break; - - case MINUTELY: - sb.append("M").append(interval); - break; - - default: - return ""; - } - - Integer count = recur.getCount(); - ICalDate until = recur.getUntil(); - sb.append(' '); - - if (count != null) { - sb.append('#').append(count); - } else if (until != null) { - String dateStr = date(until, property, context).extended(false).write(); - sb.append(dateStr); - } else { - sb.append("#0"); - } - - return sb.toString(); + return new RecurrenceWriterV1(property, context).write(); } private String writeTextV2(T property, WriteContext context) { @@ -208,7 +122,6 @@ protected T _parseText(String value, ICalDataType dataType, ICalParameters param switch (context.getVersion()) { case V1_0: - handleVersion1Multivalued(value, dataType, parameters, context); return parseTextV1(value, dataType, parameters, context); default: return parseTextV2(value, dataType, parameters, context); @@ -216,29 +129,36 @@ protected T _parseText(String value, ICalDataType dataType, ICalParameters param } /** - * Version 1.0 allows multiple RRULE values to be defined inside of the same - * property. This method checks for this and, if multiple values are found, - * parses them and throws a {@link DataModelConversionException}. * @param value the property value * @param dataType the property data type * @param parameters the property parameters * @param context the parse context + * @return the parsed property * @throws DataModelConversionException if the property contains multiple * RRULE values */ - private void handleVersion1Multivalued(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { - List rrules = splitRRULEValues(value); + private T parseTextV1(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + RecurrenceParserV1 parser = new RecurrenceParserV1(context); + + List rrules = RecurrenceParserV1.splitPropertyValue(value); if (rrules.size() == 1) { - return; + Recurrence recur = parser.parse(value); + return newInstance(recur, context, parameters); } + /* + * Version 1.0 allows multiple RRULE values to be defined inside of the + * same property. If there are multiple values, parse them and throw a + * DataModelConversionException. + */ DataModelConversionException conversionException = new DataModelConversionException(null); for (String rrule : rrules) { ICalParameters parametersCopy = new ICalParameters(parameters); ICalProperty property; try { - property = parseTextV1(rrule, dataType, parametersCopy, context); + Recurrence recur = parser.parse(rrule); + property = newInstance(recur, context, parameters); } catch (CannotParseException e) { //@formatter:off context.getWarnings().add(new ParseWarning.Builder(context) @@ -255,241 +175,6 @@ private void handleVersion1Multivalued(String value, ICalDataType dataType, ICal throw conversionException; } - /** - * Version 1.0 allows multiple RRULE values to be defined inside of the same - * property. This method extracts each RRULE value from the property value. - * @param value the property value - * @return the RRULE values - */ - private List splitRRULEValues(String value) { - List values = new ArrayList(); - Pattern p = Pattern.compile("#\\d+|\\d{8}T\\d{6}Z?"); - Matcher m = p.matcher(value); - - int prevIndex = 0; - while (m.find()) { - int end = m.end(); - String subValue = value.substring(prevIndex, end).trim(); - values.add(subValue); - prevIndex = end; - } - String subValue = value.substring(prevIndex).trim(); - if (subValue.length() > 0) { - values.add(subValue); - } - - return values; - } - - private T parseTextV1(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { - final Recurrence.Builder builder = new Recurrence.Builder((Frequency) null); - - List splitValues = Arrays.asList(value.toUpperCase().split("\\s+")); - - //parse the frequency and interval from the first token (e.g. "W2") - String frequencyStr; - Integer interval; - { - String firstToken = splitValues.get(0); - Pattern p = Pattern.compile("^([A-Z]+)(\\d+)$"); - Matcher m = p.matcher(firstToken); - if (!m.find()) { - throw new CannotParseException(40, firstToken); - } - - frequencyStr = m.group(1); - interval = integerValueOf(m.group(2)); - - splitValues = splitValues.subList(1, splitValues.size()); - } - builder.interval(interval); - - Integer count = null; - ICalDate until = null; - if (splitValues.isEmpty()) { - count = 2; - } else { - String lastToken = splitValues.get(splitValues.size() - 1); - if (lastToken.startsWith("#")) { - String countStr = lastToken.substring(1); - count = integerValueOf(countStr); - if (count == 0) { - //infinite - count = null; - } - - splitValues = splitValues.subList(0, splitValues.size() - 1); - } else { - try { - //see if the value is an "until" date - until = date(lastToken).parse(); - splitValues = splitValues.subList(0, splitValues.size() - 1); - } catch (IllegalArgumentException e) { - //last token is a regular value - count = 2; - } - } - } - builder.count(count); - builder.until(until); - - //determine what frequency enum to use and how to treat each tokenized value - Frequency frequency; - Handler handler; - if ("YD".equals(frequencyStr)) { - frequency = Frequency.YEARLY; - handler = new Handler() { - public void handle(String value) { - if (value == null) { - return; - } - - Integer dayOfYear = integerValueOf(value); - builder.byYearDay(dayOfYear); - } - }; - } else if ("YM".equals(frequencyStr)) { - frequency = Frequency.YEARLY; - handler = new Handler() { - public void handle(String value) { - if (value == null) { - return; - } - - Integer month = integerValueOf(value); - builder.byMonth(month); - } - }; - } else if ("MD".equals(frequencyStr)) { - frequency = Frequency.MONTHLY; - handler = new Handler() { - public void handle(String value) { - if (value == null) { - return; - } - - try { - Integer date = "LD".equals(value) ? -1 : parseVCalInt(value); - builder.byMonthDay(date); - } catch (NumberFormatException e) { - throw new CannotParseException(40, value); - } - } - }; - } else if ("MP".equals(frequencyStr)) { - frequency = Frequency.MONTHLY; - handler = new Handler() { - private final List nums = new ArrayList(); - private final List days = new ArrayList(); - private boolean readNum = false; - - public void handle(String value) { - if (value == null) { - //end of list - for (Integer num : nums) { - for (DayOfWeek day : days) { - builder.byDay(num, day); - } - } - return; - } - - if (value.matches("\\d{4}")) { - readNum = false; - - Integer hour = integerValueOf(value.substring(0, 2)); - builder.byHour(hour); - - Integer minute = integerValueOf(value.substring(2, 4)); - builder.byMinute(minute); - return; - } - - try { - Integer curNum = parseVCalInt(value); - - if (!readNum) { - //reset lists, new segment - for (Integer num : nums) { - for (DayOfWeek day : days) { - builder.byDay(num, day); - } - } - nums.clear(); - days.clear(); - - readNum = true; - } - - nums.add(curNum); - } catch (NumberFormatException e) { - readNum = false; - - DayOfWeek day = parseDay(value); - days.add(day); - } - } - }; - } else if ("W".equals(frequencyStr)) { - frequency = Frequency.WEEKLY; - handler = new Handler() { - public void handle(String value) { - if (value == null) { - return; - } - - DayOfWeek day = parseDay(value); - builder.byDay(day); - } - }; - } else if ("D".equals(frequencyStr)) { - frequency = Frequency.DAILY; - handler = new Handler() { - public void handle(String value) { - if (value == null) { - return; - } - - Integer hour = integerValueOf(value.substring(0, 2)); - builder.byHour(hour); - - Integer minute = integerValueOf(value.substring(2, 4)); - builder.byMinute(minute); - } - }; - } else if ("M".equals(frequencyStr)) { - frequency = Frequency.MINUTELY; - handler = new Handler() { - public void handle(String value) { - //TODO can this ever have values? - } - }; - } else { - throw new CannotParseException(41, frequencyStr); - } - - builder.frequency(frequency); - - //parse the rest of the tokens - for (String splitValue : splitValues) { - //TODO not sure how to handle the "$" symbol, ignore it - if (splitValue.endsWith("$")) { - context.addWarning(36, splitValue); - splitValue = splitValue.substring(0, splitValue.length() - 1); - } - - handler.handle(splitValue); - } - handler.handle(null); - - T property = newInstance(builder.build()); - if (until != null) { - context.addDate(until, property, parameters); - } - - return property; - } - private T parseTextV2(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { Recurrence.Builder builder = new Recurrence.Builder((Frequency) null); ListMultimap rules = new ListMultimap(VObjectPropertyValues.parseMultimap(value)); @@ -510,70 +195,7 @@ private T parseTextV2(String value, ICalDataType dataType, ICalParameters parame parseWkst(rules, builder, context); parseXRules(rules, builder); //must be called last - T property = newInstance(builder.build()); - - ICalDate until = property.getValue().getUntil(); - if (until != null) { - context.addDate(until, property, parameters); - } - - return property; - } - - /** - * Parses an integer string, where the sign is at the end of the string - * instead of at the beginning (for example, "5-"). - * @param value the string - * @return the value - * @throws NumberFormatException if the string cannot be parsed as an - * integer - */ - private static int parseVCalInt(String value) { - int negate = 1; - if (value.endsWith("+")) { - value = value.substring(0, value.length() - 1); - } else if (value.endsWith("-")) { - value = value.substring(0, value.length() - 1); - negate = -1; - } - - return Integer.parseInt(value) * negate; - } - - /** - * Same as {@link Integer#valueOf(String)}, but throws a - * {@link CannotParseException} when it fails. - * @param value the string to parse - * @return the parse integer - * @throws CannotParseException if the string cannot be parsed - */ - private static Integer integerValueOf(String value) { - try { - return Integer.valueOf(value); - } catch (NumberFormatException e) { - throw new CannotParseException(40, value); - } - } - - private static String writeVCalInt(Integer value) { - if (value > 0) { - return value + "+"; - } - - if (value < 0) { - return Math.abs(value) + "-"; - } - - return value.toString(); - } - - private DayOfWeek parseDay(String value) { - DayOfWeek day = DayOfWeek.valueOfAbbr(value); - if (day == null) { - throw new CannotParseException(42, value); - } - - return day; + return newInstance(builder.build(), context, parameters); } @Override @@ -705,6 +327,16 @@ protected T _parseJson(JCalValue value, ICalDataType dataType, ICalParameters pa */ protected abstract T newInstance(Recurrence recur); + private T newInstance(Recurrence recur, ParseContext context, ICalParameters parameters) { + T property = newInstance(recur); + + if (recur.getUntil() != null) { + context.addDate(recur.getUntil(), property, parameters); + } + + return property; + } + private void parseFreq(ListMultimap rules, final Recurrence.Builder builder, final ParseContext context) { parseFirst(rules, FREQ, new Handler() { public void handle(String value) { diff --git a/src/main/java/biweekly/io/scribe/property/RecurrenceWriterV1.java b/src/main/java/biweekly/io/scribe/property/RecurrenceWriterV1.java new file mode 100644 index 00000000..fb97417f --- /dev/null +++ b/src/main/java/biweekly/io/scribe/property/RecurrenceWriterV1.java @@ -0,0 +1,129 @@ +package biweekly.io.scribe.property; + +import static biweekly.io.scribe.property.ICalPropertyScribe.date; + +import biweekly.io.WriteContext; +import biweekly.property.RecurrenceProperty; +import biweekly.util.ByDay; +import biweekly.util.DayOfWeek; +import biweekly.util.Frequency; +import biweekly.util.ICalDate; +import biweekly.util.Recurrence; + +/** + * Writes iCal 1.0 RRULE values. + * @author Michael Angstadt + */ +class RecurrenceWriterV1 { + private final RecurrenceProperty property; + private final WriteContext context; + + public RecurrenceWriterV1(RecurrenceProperty property, WriteContext context) { + this.property = property; + this.context = context; + } + + public String write() { + Recurrence recur = property.getValue(); + Frequency frequency = recur.getFrequency(); + if (frequency == null) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + + Integer interval = recur.getInterval(); + if (interval == null) { + interval = 1; + } + + switch (frequency) { + case YEARLY: + if (!recur.getByMonth().isEmpty()) { + sb.append("YM").append(interval); + for (Integer month : recur.getByMonth()) { + sb.append(' ').append(month); + } + } else { + sb.append("YD").append(interval); + for (Integer day : recur.getByYearDay()) { + sb.append(' ').append(day); + } + } + break; + + case MONTHLY: + if (!recur.getByMonthDay().isEmpty()) { + sb.append("MD").append(interval); + for (Integer day : recur.getByMonthDay()) { + sb.append(' ').append(writeVCalInt(day)); + } + } else { + sb.append("MP").append(interval); + for (ByDay byDay : recur.getByDay()) { + DayOfWeek day = byDay.getDay(); + Integer prefix = byDay.getNum(); + if (prefix == null) { + prefix = 1; + } + + sb.append(' ').append(writeVCalInt(prefix)).append(' ').append(day.getAbbr()); + } + } + break; + + case WEEKLY: + sb.append("W").append(interval); + for (ByDay byDay : recur.getByDay()) { + sb.append(' ').append(byDay.getDay().getAbbr()); + } + break; + + case DAILY: + sb.append("D").append(interval); + break; + + case HOURLY: + sb.append("M").append(interval * 60); + break; + + case MINUTELY: + sb.append("M").append(interval); + break; + + default: + return ""; + } + + writeCountOrUntil(recur, property, sb); + + return sb.toString(); + } + + private void writeCountOrUntil(Recurrence recur, RecurrenceProperty property, StringBuilder sb) { + Integer count = recur.getCount(); + ICalDate until = recur.getUntil(); + sb.append(' '); + + if (count != null) { + sb.append('#').append(count); + } else if (until != null) { + String dateStr = date(until, property, context).extended(false).write(); + sb.append(dateStr); + } else { + sb.append("#0"); //infinite + } + } + + private String writeVCalInt(Integer value) { + if (value > 0) { + return value + "+"; + } + + if (value < 0) { + return Math.abs(value) + "-"; + } + + return value.toString(); + } +}