1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.calendarcommon; 18 19 import android.text.TextUtils; 20 import android.text.format.Time; 21 import android.util.Log; 22 import android.util.TimeFormatException; 23 24 import java.util.Calendar; 25 import java.util.HashMap; 26 27 /** 28 * Event recurrence utility functions. 29 */ 30 public class EventRecurrence { 31 private static String TAG = "EventRecur"; 32 33 public static final int SECONDLY = 1; 34 public static final int MINUTELY = 2; 35 public static final int HOURLY = 3; 36 public static final int DAILY = 4; 37 public static final int WEEKLY = 5; 38 public static final int MONTHLY = 6; 39 public static final int YEARLY = 7; 40 41 public static final int SU = 0x00010000; 42 public static final int MO = 0x00020000; 43 public static final int TU = 0x00040000; 44 public static final int WE = 0x00080000; 45 public static final int TH = 0x00100000; 46 public static final int FR = 0x00200000; 47 public static final int SA = 0x00400000; 48 49 public Time startDate; // set by setStartDate(), not parse() 50 51 public int freq; // SECONDLY, MINUTELY, etc. 52 public String until; 53 public int count; 54 public int interval; 55 public int wkst; // SU, MO, TU, etc. 56 57 /* lists with zero entries may be null references */ 58 public int[] bysecond; 59 public int bysecondCount; 60 public int[] byminute; 61 public int byminuteCount; 62 public int[] byhour; 63 public int byhourCount; 64 public int[] byday; 65 public int[] bydayNum; 66 public int bydayCount; 67 public int[] bymonthday; 68 public int bymonthdayCount; 69 public int[] byyearday; 70 public int byyeardayCount; 71 public int[] byweekno; 72 public int byweeknoCount; 73 public int[] bymonth; 74 public int bymonthCount; 75 public int[] bysetpos; 76 public int bysetposCount; 77 78 /** maps a part string to a parser object */ 79 private static HashMap<String,PartParser> sParsePartMap; 80 static { 81 sParsePartMap = new HashMap<String,PartParser>(); 82 sParsePartMap.put("FREQ", new ParseFreq()); 83 sParsePartMap.put("UNTIL", new ParseUntil()); 84 sParsePartMap.put("COUNT", new ParseCount()); 85 sParsePartMap.put("INTERVAL", new ParseInterval()); 86 sParsePartMap.put("BYSECOND", new ParseBySecond()); 87 sParsePartMap.put("BYMINUTE", new ParseByMinute()); 88 sParsePartMap.put("BYHOUR", new ParseByHour()); 89 sParsePartMap.put("BYDAY", new ParseByDay()); 90 sParsePartMap.put("BYMONTHDAY", new ParseByMonthDay()); 91 sParsePartMap.put("BYYEARDAY", new ParseByYearDay()); 92 sParsePartMap.put("BYWEEKNO", new ParseByWeekNo()); 93 sParsePartMap.put("BYMONTH", new ParseByMonth()); 94 sParsePartMap.put("BYSETPOS", new ParseBySetPos()); 95 sParsePartMap.put("WKST", new ParseWkst()); 96 } 97 98 /* values for bit vector that keeps track of what we have already seen */ 99 private static final int PARSED_FREQ = 1 << 0; 100 private static final int PARSED_UNTIL = 1 << 1; 101 private static final int PARSED_COUNT = 1 << 2; 102 private static final int PARSED_INTERVAL = 1 << 3; 103 private static final int PARSED_BYSECOND = 1 << 4; 104 private static final int PARSED_BYMINUTE = 1 << 5; 105 private static final int PARSED_BYHOUR = 1 << 6; 106 private static final int PARSED_BYDAY = 1 << 7; 107 private static final int PARSED_BYMONTHDAY = 1 << 8; 108 private static final int PARSED_BYYEARDAY = 1 << 9; 109 private static final int PARSED_BYWEEKNO = 1 << 10; 110 private static final int PARSED_BYMONTH = 1 << 11; 111 private static final int PARSED_BYSETPOS = 1 << 12; 112 private static final int PARSED_WKST = 1 << 13; 113 114 /** maps a FREQ value to an integer constant */ 115 private static final HashMap<String,Integer> sParseFreqMap = new HashMap<String,Integer>(); 116 static { 117 sParseFreqMap.put("SECONDLY", SECONDLY); 118 sParseFreqMap.put("MINUTELY", MINUTELY); 119 sParseFreqMap.put("HOURLY", HOURLY); 120 sParseFreqMap.put("DAILY", DAILY); 121 sParseFreqMap.put("WEEKLY", WEEKLY); 122 sParseFreqMap.put("MONTHLY", MONTHLY); 123 sParseFreqMap.put("YEARLY", YEARLY); 124 } 125 126 /** maps a two-character weekday string to an integer constant */ 127 private static final HashMap<String,Integer> sParseWeekdayMap = new HashMap<String,Integer>(); 128 static { 129 sParseWeekdayMap.put("SU", SU); 130 sParseWeekdayMap.put("MO", MO); 131 sParseWeekdayMap.put("TU", TU); 132 sParseWeekdayMap.put("WE", WE); 133 sParseWeekdayMap.put("TH", TH); 134 sParseWeekdayMap.put("FR", FR); 135 sParseWeekdayMap.put("SA", SA); 136 } 137 138 /** If set, allow lower-case recurrence rule strings. Minor performance impact. */ 139 private static final boolean ALLOW_LOWER_CASE = false; 140 141 /** If set, validate the value of UNTIL parts. Minor performance impact. */ 142 private static final boolean VALIDATE_UNTIL = false; 143 144 /** If set, require that only one of {UNTIL,COUNT} is present. Breaks compat w/ old parser. */ 145 private static final boolean ONLY_ONE_UNTIL_COUNT = false; 146 147 148 /** 149 * Thrown when a recurrence string provided can not be parsed according 150 * to RFC2445. 151 */ 152 public static class InvalidFormatException extends RuntimeException { 153 InvalidFormatException(String s) { 154 super(s); 155 } 156 } 157 158 159 public void setStartDate(Time date) { 160 startDate = date; 161 } 162 163 /** 164 * Converts one of the Calendar.SUNDAY constants to the SU, MO, etc. 165 * constants. btw, I think we should switch to those here too, to 166 * get rid of this function, if possible. 167 */ 168 public static int calendarDay2Day(int day) 169 { 170 switch (day) 171 { 172 case Calendar.SUNDAY: 173 return SU; 174 case Calendar.MONDAY: 175 return MO; 176 case Calendar.TUESDAY: 177 return TU; 178 case Calendar.WEDNESDAY: 179 return WE; 180 case Calendar.THURSDAY: 181 return TH; 182 case Calendar.FRIDAY: 183 return FR; 184 case Calendar.SATURDAY: 185 return SA; 186 default: 187 throw new RuntimeException("bad day of week: " + day); 188 } 189 } 190 191 public static int timeDay2Day(int day) 192 { 193 switch (day) 194 { 195 case Time.SUNDAY: 196 return SU; 197 case Time.MONDAY: 198 return MO; 199 case Time.TUESDAY: 200 return TU; 201 case Time.WEDNESDAY: 202 return WE; 203 case Time.THURSDAY: 204 return TH; 205 case Time.FRIDAY: 206 return FR; 207 case Time.SATURDAY: 208 return SA; 209 default: 210 throw new RuntimeException("bad day of week: " + day); 211 } 212 } 213 public static int day2TimeDay(int day) 214 { 215 switch (day) 216 { 217 case SU: 218 return Time.SUNDAY; 219 case MO: 220 return Time.MONDAY; 221 case TU: 222 return Time.TUESDAY; 223 case WE: 224 return Time.WEDNESDAY; 225 case TH: 226 return Time.THURSDAY; 227 case FR: 228 return Time.FRIDAY; 229 case SA: 230 return Time.SATURDAY; 231 default: 232 throw new RuntimeException("bad day of week: " + day); 233 } 234 } 235 236 /** 237 * Converts one of the SU, MO, etc. constants to the Calendar.SUNDAY 238 * constants. btw, I think we should switch to those here too, to 239 * get rid of this function, if possible. 240 */ 241 public static int day2CalendarDay(int day) 242 { 243 switch (day) 244 { 245 case SU: 246 return Calendar.SUNDAY; 247 case MO: 248 return Calendar.MONDAY; 249 case TU: 250 return Calendar.TUESDAY; 251 case WE: 252 return Calendar.WEDNESDAY; 253 case TH: 254 return Calendar.THURSDAY; 255 case FR: 256 return Calendar.FRIDAY; 257 case SA: 258 return Calendar.SATURDAY; 259 default: 260 throw new RuntimeException("bad day of week: " + day); 261 } 262 } 263 264 /** 265 * Converts one of the internal day constants (SU, MO, etc.) to the 266 * two-letter string representing that constant. 267 * 268 * @param day one the internal constants SU, MO, etc. 269 * @return the two-letter string for the day ("SU", "MO", etc.) 270 * 271 * @throws IllegalArgumentException Thrown if the day argument is not one of 272 * the defined day constants. 273 */ 274 private static String day2String(int day) { 275 switch (day) { 276 case SU: 277 return "SU"; 278 case MO: 279 return "MO"; 280 case TU: 281 return "TU"; 282 case WE: 283 return "WE"; 284 case TH: 285 return "TH"; 286 case FR: 287 return "FR"; 288 case SA: 289 return "SA"; 290 default: 291 throw new IllegalArgumentException("bad day argument: " + day); 292 } 293 } 294 295 private static void appendNumbers(StringBuilder s, String label, 296 int count, int[] values) 297 { 298 if (count > 0) { 299 s.append(label); 300 count--; 301 for (int i=0; i<count; i++) { 302 s.append(values[i]); 303 s.append(","); 304 } 305 s.append(values[count]); 306 } 307 } 308 309 private void appendByDay(StringBuilder s, int i) 310 { 311 int n = this.bydayNum[i]; 312 if (n != 0) { 313 s.append(n); 314 } 315 316 String str = day2String(this.byday[i]); 317 s.append(str); 318 } 319 320 @Override 321 public String toString() 322 { 323 StringBuilder s = new StringBuilder(); 324 325 s.append("FREQ="); 326 switch (this.freq) 327 { 328 case SECONDLY: 329 s.append("SECONDLY"); 330 break; 331 case MINUTELY: 332 s.append("MINUTELY"); 333 break; 334 case HOURLY: 335 s.append("HOURLY"); 336 break; 337 case DAILY: 338 s.append("DAILY"); 339 break; 340 case WEEKLY: 341 s.append("WEEKLY"); 342 break; 343 case MONTHLY: 344 s.append("MONTHLY"); 345 break; 346 case YEARLY: 347 s.append("YEARLY"); 348 break; 349 } 350 351 if (!TextUtils.isEmpty(this.until)) { 352 s.append(";UNTIL="); 353 s.append(until); 354 } 355 356 if (this.count != 0) { 357 s.append(";COUNT="); 358 s.append(this.count); 359 } 360 361 if (this.interval != 0) { 362 s.append(";INTERVAL="); 363 s.append(this.interval); 364 } 365 366 if (this.wkst != 0) { 367 s.append(";WKST="); 368 s.append(day2String(this.wkst)); 369 } 370 371 appendNumbers(s, ";BYSECOND=", this.bysecondCount, this.bysecond); 372 appendNumbers(s, ";BYMINUTE=", this.byminuteCount, this.byminute); 373 appendNumbers(s, ";BYSECOND=", this.byhourCount, this.byhour); 374 375 // day 376 int count = this.bydayCount; 377 if (count > 0) { 378 s.append(";BYDAY="); 379 count--; 380 for (int i=0; i<count; i++) { 381 appendByDay(s, i); 382 s.append(","); 383 } 384 appendByDay(s, count); 385 } 386 387 appendNumbers(s, ";BYMONTHDAY=", this.bymonthdayCount, this.bymonthday); 388 appendNumbers(s, ";BYYEARDAY=", this.byyeardayCount, this.byyearday); 389 appendNumbers(s, ";BYWEEKNO=", this.byweeknoCount, this.byweekno); 390 appendNumbers(s, ";BYMONTH=", this.bymonthCount, this.bymonth); 391 appendNumbers(s, ";BYSETPOS=", this.bysetposCount, this.bysetpos); 392 393 return s.toString(); 394 } 395 396 public boolean repeatsOnEveryWeekDay() { 397 if (this.freq != WEEKLY) { 398 return false; 399 } 400 401 int count = this.bydayCount; 402 if (count != 5) { 403 return false; 404 } 405 406 for (int i = 0 ; i < count ; i++) { 407 int day = byday[i]; 408 if (day == SU || day == SA) { 409 return false; 410 } 411 } 412 413 return true; 414 } 415 416 /** 417 * Determines whether this rule specifies a simple monthly rule by weekday, such as 418 * "FREQ=MONTHLY;BYDAY=3TU" (the 3rd Tuesday of every month). 419 * <p> 420 * Negative days, e.g. "FREQ=MONTHLY;BYDAY=-1TU" (the last Tuesday of every month), 421 * will cause "false" to be returned. 422 * <p> 423 * Rules that fire every week, such as "FREQ=MONTHLY;BYDAY=TU" (every Tuesday of every 424 * month) will cause "false" to be returned. (Note these are usually expressed as 425 * WEEKLY rules, and hence are uncommon.) 426 * 427 * @return true if this rule is of the appropriate form 428 */ 429 public boolean repeatsMonthlyOnDayCount() { 430 if (this.freq != MONTHLY) { 431 return false; 432 } 433 434 if (bydayCount != 1 || bymonthdayCount != 0) { 435 return false; 436 } 437 438 if (bydayNum[0] <= 0) { 439 return false; 440 } 441 442 return true; 443 } 444 445 /** 446 * Determines whether two integer arrays contain identical elements. 447 * <p> 448 * The native implementation over-allocated the arrays (and may have stuff left over from 449 * a previous run), so we can't just check the arrays -- the separately-maintained count 450 * field also matters. We assume that a null array will have a count of zero, and that the 451 * array can hold as many elements as the associated count indicates. 452 * <p> 453 * TODO: replace this with Arrays.equals() when the old parser goes away. 454 */ 455 private static boolean arraysEqual(int[] array1, int count1, int[] array2, int count2) { 456 if (count1 != count2) { 457 return false; 458 } 459 460 for (int i = 0; i < count1; i++) { 461 if (array1[i] != array2[i]) 462 return false; 463 } 464 465 return true; 466 } 467 468 @Override 469 public boolean equals(Object obj) { 470 if (this == obj) { 471 return true; 472 } 473 if (!(obj instanceof EventRecurrence)) { 474 return false; 475 } 476 477 EventRecurrence er = (EventRecurrence) obj; 478 return (startDate == null ? 479 er.startDate == null : Time.compare(startDate, er.startDate) == 0) && 480 freq == er.freq && 481 (until == null ? er.until == null : until.equals(er.until)) && 482 count == er.count && 483 interval == er.interval && 484 wkst == er.wkst && 485 arraysEqual(bysecond, bysecondCount, er.bysecond, er.bysecondCount) && 486 arraysEqual(byminute, byminuteCount, er.byminute, er.byminuteCount) && 487 arraysEqual(byhour, byhourCount, er.byhour, er.byhourCount) && 488 arraysEqual(byday, bydayCount, er.byday, er.bydayCount) && 489 arraysEqual(bydayNum, bydayCount, er.bydayNum, er.bydayCount) && 490 arraysEqual(bymonthday, bymonthdayCount, er.bymonthday, er.bymonthdayCount) && 491 arraysEqual(byyearday, byyeardayCount, er.byyearday, er.byyeardayCount) && 492 arraysEqual(byweekno, byweeknoCount, er.byweekno, er.byweeknoCount) && 493 arraysEqual(bymonth, bymonthCount, er.bymonth, er.bymonthCount) && 494 arraysEqual(bysetpos, bysetposCount, er.bysetpos, er.bysetposCount); 495 } 496 497 @Override public int hashCode() { 498 // We overrode equals, so we must override hashCode(). Nobody seems to need this though. 499 throw new UnsupportedOperationException(); 500 } 501 502 /** 503 * Resets parser-modified fields to their initial state. Does not alter startDate. 504 * <p> 505 * The original parser always set all of the "count" fields, "wkst", and "until", 506 * essentially allowing the same object to be used multiple times by calling parse(). 507 * It's unclear whether this behavior was intentional. For now, be paranoid and 508 * preserve the existing behavior by resetting the fields. 509 * <p> 510 * We don't need to touch the integer arrays; they will either be ignored or 511 * overwritten. The "startDate" field is not set by the parser, so we ignore it here. 512 */ 513 private void resetFields() { 514 until = null; 515 freq = count = interval = bysecondCount = byminuteCount = byhourCount = 516 bydayCount = bymonthdayCount = byyeardayCount = byweeknoCount = bymonthCount = 517 bysetposCount = 0; 518 } 519 520 /** 521 * Parses an rfc2445 recurrence rule string into its component pieces. Attempting to parse 522 * malformed input will result in an EventRecurrence.InvalidFormatException. 523 * 524 * @param recur The recurrence rule to parse (in un-folded form). 525 */ 526 public void parse(String recur) { 527 /* 528 * From RFC 2445 section 4.3.10: 529 * 530 * recur = "FREQ"=freq *( 531 * ; either UNTIL or COUNT may appear in a 'recur', 532 * ; but UNTIL and COUNT MUST NOT occur in the same 'recur' 533 * 534 * ( ";" "UNTIL" "=" enddate ) / 535 * ( ";" "COUNT" "=" 1*DIGIT ) / 536 * 537 * ; the rest of these keywords are optional, 538 * ; but MUST NOT occur more than once 539 * 540 * ( ";" "INTERVAL" "=" 1*DIGIT ) / 541 * ( ";" "BYSECOND" "=" byseclist ) / 542 * ( ";" "BYMINUTE" "=" byminlist ) / 543 * ( ";" "BYHOUR" "=" byhrlist ) / 544 * ( ";" "BYDAY" "=" bywdaylist ) / 545 * ( ";" "BYMONTHDAY" "=" bymodaylist ) / 546 * ( ";" "BYYEARDAY" "=" byyrdaylist ) / 547 * ( ";" "BYWEEKNO" "=" bywknolist ) / 548 * ( ";" "BYMONTH" "=" bymolist ) / 549 * ( ";" "BYSETPOS" "=" bysplist ) / 550 * ( ";" "WKST" "=" weekday ) / 551 * ( ";" x-name "=" text ) 552 * ) 553 * 554 * The rule parts are not ordered in any particular sequence. 555 * 556 * Examples: 557 * FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU 558 * FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8 559 * 560 * Strategy: 561 * (1) Split the string at ';' boundaries to get an array of rule "parts". 562 * (2) For each part, find substrings for left/right sides of '=' (name/value). 563 * (3) Call a <name>-specific parsing function to parse the <value> into an 564 * output field. 565 * 566 * By keeping track of which names we've seen in a bit vector, we can verify the 567 * constraints indicated above (FREQ appears first, none of them appear more than once -- 568 * though x-[name] would require special treatment), and we have either UNTIL or COUNT 569 * but not both. 570 * 571 * In general, RFC 2445 property names (e.g. "FREQ") and enumerations ("TU") must 572 * be handled in a case-insensitive fashion, but case may be significant for other 573 * properties. We don't have any case-sensitive values in RRULE, except possibly 574 * for the custom "X-" properties, but we ignore those anyway. Thus, we can trivially 575 * convert the entire string to upper case and then use simple comparisons. 576 * 577 * Differences from previous version: 578 * - allows lower-case property and enumeration values [optional] 579 * - enforces that FREQ appears first 580 * - enforces that only one of UNTIL and COUNT may be specified 581 * - allows (but ignores) X-* parts 582 * - improved validation on various values (e.g. UNTIL timestamps) 583 * - error messages are more specific 584 */ 585 586 /* TODO: replace with "if (freq != 0) throw" if nothing requires this */ 587 resetFields(); 588 589 int parseFlags = 0; 590 String[] parts; 591 if (ALLOW_LOWER_CASE) { 592 parts = recur.toUpperCase().split(";"); 593 } else { 594 parts = recur.split(";"); 595 } 596 for (String part : parts) { 597 int equalIndex = part.indexOf('='); 598 if (equalIndex <= 0) { 599 /* no '=' or no LHS */ 600 throw new InvalidFormatException("Missing LHS in " + part); 601 } 602 603 String lhs = part.substring(0, equalIndex); 604 String rhs = part.substring(equalIndex + 1); 605 if (rhs.length() == 0) { 606 throw new InvalidFormatException("Missing RHS in " + part); 607 } 608 609 /* 610 * In lieu of a "switch" statement that allows string arguments, we use a 611 * map from strings to parsing functions. 612 */ 613 PartParser parser = sParsePartMap.get(lhs); 614 if (parser == null) { 615 if (lhs.startsWith("X-")) { 616 //Log.d(TAG, "Ignoring custom part " + lhs); 617 continue; 618 } 619 throw new InvalidFormatException("Couldn't find parser for " + lhs); 620 } else { 621 int flag = parser.parsePart(rhs, this); 622 if ((parseFlags & flag) != 0) { 623 throw new InvalidFormatException("Part " + lhs + " was specified twice"); 624 } 625 parseFlags |= flag; 626 } 627 } 628 629 // If not specified, week starts on Monday. 630 if ((parseFlags & PARSED_WKST) == 0) { 631 wkst = MO; 632 } 633 634 // FREQ is mandatory. 635 if ((parseFlags & PARSED_FREQ) == 0) { 636 throw new InvalidFormatException("Must specify a FREQ value"); 637 } 638 639 // Can't have both UNTIL and COUNT. 640 if ((parseFlags & (PARSED_UNTIL | PARSED_COUNT)) == (PARSED_UNTIL | PARSED_COUNT)) { 641 if (ONLY_ONE_UNTIL_COUNT) { 642 throw new InvalidFormatException("Must not specify both UNTIL and COUNT: " + recur); 643 } else { 644 Log.w(TAG, "Warning: rrule has both UNTIL and COUNT: " + recur); 645 } 646 } 647 } 648 649 /** 650 * Base class for the RRULE part parsers. 651 */ 652 abstract static class PartParser { 653 /** 654 * Parses a single part. 655 * 656 * @param value The right-hand-side of the part. 657 * @param er The EventRecurrence into which the result is stored. 658 * @return A bit value indicating which part was parsed. 659 */ 660 public abstract int parsePart(String value, EventRecurrence er); 661 662 /** 663 * Parses an integer, with range-checking. 664 * 665 * @param str The string to parse. 666 * @param minVal Minimum allowed value. 667 * @param maxVal Maximum allowed value. 668 * @param allowZero Is 0 allowed? 669 * @return The parsed value. 670 */ 671 public static int parseIntRange(String str, int minVal, int maxVal, boolean allowZero) { 672 try { 673 if (str.charAt(0) == '+') { 674 // Integer.parseInt does not allow a leading '+', so skip it manually. 675 str = str.substring(1); 676 } 677 int val = Integer.parseInt(str); 678 if (val < minVal || val > maxVal || (val == 0 && !allowZero)) { 679 throw new InvalidFormatException("Integer value out of range: " + str); 680 } 681 return val; 682 } catch (NumberFormatException nfe) { 683 throw new InvalidFormatException("Invalid integer value: " + str); 684 } 685 } 686 687 /** 688 * Parses a comma-separated list of integers, with range-checking. 689 * 690 * @param listStr The string to parse. 691 * @param minVal Minimum allowed value. 692 * @param maxVal Maximum allowed value. 693 * @param allowZero Is 0 allowed? 694 * @return A new array with values, sized to hold the exact number of elements. 695 */ 696 public static int[] parseNumberList(String listStr, int minVal, int maxVal, 697 boolean allowZero) { 698 int[] values; 699 700 if (listStr.indexOf(",") < 0) { 701 // Common case: only one entry, skip split() overhead. 702 values = new int[1]; 703 values[0] = parseIntRange(listStr, minVal, maxVal, allowZero); 704 } else { 705 String[] valueStrs = listStr.split(","); 706 int len = valueStrs.length; 707 values = new int[len]; 708 for (int i = 0; i < len; i++) { 709 values[i] = parseIntRange(valueStrs[i], minVal, maxVal, allowZero); 710 } 711 } 712 return values; 713 } 714 } 715 716 /** parses FREQ={SECONDLY,MINUTELY,...} */ 717 private static class ParseFreq extends PartParser { 718 @Override public int parsePart(String value, EventRecurrence er) { 719 Integer freq = sParseFreqMap.get(value); 720 if (freq == null) { 721 throw new InvalidFormatException("Invalid FREQ value: " + value); 722 } 723 er.freq = freq; 724 return PARSED_FREQ; 725 } 726 } 727 /** parses UNTIL=enddate, e.g. "19970829T021400" */ 728 private static class ParseUntil extends PartParser { 729 @Override public int parsePart(String value, EventRecurrence er) { 730 if (VALIDATE_UNTIL) { 731 try { 732 // Parse the time to validate it. The result isn't retained. 733 Time until = new Time(); 734 until.parse(value); 735 } catch (TimeFormatException tfe) { 736 throw new InvalidFormatException("Invalid UNTIL value: " + value); 737 } 738 } 739 er.until = value; 740 return PARSED_UNTIL; 741 } 742 } 743 /** parses COUNT=[non-negative-integer] */ 744 private static class ParseCount extends PartParser { 745 @Override public int parsePart(String value, EventRecurrence er) { 746 er.count = parseIntRange(value, 0, Integer.MAX_VALUE, true); 747 return PARSED_COUNT; 748 } 749 } 750 /** parses INTERVAL=[non-negative-integer] */ 751 private static class ParseInterval extends PartParser { 752 @Override public int parsePart(String value, EventRecurrence er) { 753 er.interval = parseIntRange(value, 1, Integer.MAX_VALUE, false); 754 return PARSED_INTERVAL; 755 } 756 } 757 /** parses BYSECOND=byseclist */ 758 private static class ParseBySecond extends PartParser { 759 @Override public int parsePart(String value, EventRecurrence er) { 760 int[] bysecond = parseNumberList(value, 0, 59, true); 761 er.bysecond = bysecond; 762 er.bysecondCount = bysecond.length; 763 return PARSED_BYSECOND; 764 } 765 } 766 /** parses BYMINUTE=byminlist */ 767 private static class ParseByMinute extends PartParser { 768 @Override public int parsePart(String value, EventRecurrence er) { 769 int[] byminute = parseNumberList(value, 0, 59, true); 770 er.byminute = byminute; 771 er.byminuteCount = byminute.length; 772 return PARSED_BYMINUTE; 773 } 774 } 775 /** parses BYHOUR=byhrlist */ 776 private static class ParseByHour extends PartParser { 777 @Override public int parsePart(String value, EventRecurrence er) { 778 int[] byhour = parseNumberList(value, 0, 23, true); 779 er.byhour = byhour; 780 er.byhourCount = byhour.length; 781 return PARSED_BYHOUR; 782 } 783 } 784 /** parses BYDAY=bywdaylist, e.g. "1SU,-1SU" */ 785 private static class ParseByDay extends PartParser { 786 @Override public int parsePart(String value, EventRecurrence er) { 787 int[] byday; 788 int[] bydayNum; 789 int bydayCount; 790 791 if (value.indexOf(",") < 0) { 792 /* only one entry, skip split() overhead */ 793 bydayCount = 1; 794 byday = new int[1]; 795 bydayNum = new int[1]; 796 parseWday(value, byday, bydayNum, 0); 797 } else { 798 String[] wdays = value.split(","); 799 int len = wdays.length; 800 bydayCount = len; 801 byday = new int[len]; 802 bydayNum = new int[len]; 803 for (int i = 0; i < len; i++) { 804 parseWday(wdays[i], byday, bydayNum, i); 805 } 806 } 807 er.byday = byday; 808 er.bydayNum = bydayNum; 809 er.bydayCount = bydayCount; 810 return PARSED_BYDAY; 811 } 812 813 /** parses [int]weekday, putting the pieces into parallel array entries */ 814 private static void parseWday(String str, int[] byday, int[] bydayNum, int index) { 815 int wdayStrStart = str.length() - 2; 816 String wdayStr; 817 818 if (wdayStrStart > 0) { 819 /* number is included; parse it out and advance to weekday */ 820 String numPart = str.substring(0, wdayStrStart); 821 int num = parseIntRange(numPart, -53, 53, false); 822 bydayNum[index] = num; 823 wdayStr = str.substring(wdayStrStart); 824 } else { 825 /* just the weekday string */ 826 wdayStr = str; 827 } 828 Integer wday = sParseWeekdayMap.get(wdayStr); 829 if (wday == null) { 830 throw new InvalidFormatException("Invalid BYDAY value: " + str); 831 } 832 byday[index] = wday; 833 } 834 } 835 /** parses BYMONTHDAY=bymodaylist */ 836 private static class ParseByMonthDay extends PartParser { 837 @Override public int parsePart(String value, EventRecurrence er) { 838 int[] bymonthday = parseNumberList(value, -31, 31, false); 839 er.bymonthday = bymonthday; 840 er.bymonthdayCount = bymonthday.length; 841 return PARSED_BYMONTHDAY; 842 } 843 } 844 /** parses BYYEARDAY=byyrdaylist */ 845 private static class ParseByYearDay extends PartParser { 846 @Override public int parsePart(String value, EventRecurrence er) { 847 int[] byyearday = parseNumberList(value, -366, 366, false); 848 er.byyearday = byyearday; 849 er.byyeardayCount = byyearday.length; 850 return PARSED_BYYEARDAY; 851 } 852 } 853 /** parses BYWEEKNO=bywknolist */ 854 private static class ParseByWeekNo extends PartParser { 855 @Override public int parsePart(String value, EventRecurrence er) { 856 int[] byweekno = parseNumberList(value, -53, 53, false); 857 er.byweekno = byweekno; 858 er.byweeknoCount = byweekno.length; 859 return PARSED_BYWEEKNO; 860 } 861 } 862 /** parses BYMONTH=bymolist */ 863 private static class ParseByMonth extends PartParser { 864 @Override public int parsePart(String value, EventRecurrence er) { 865 int[] bymonth = parseNumberList(value, 1, 12, false); 866 er.bymonth = bymonth; 867 er.bymonthCount = bymonth.length; 868 return PARSED_BYMONTH; 869 } 870 } 871 /** parses BYSETPOS=bysplist */ 872 private static class ParseBySetPos extends PartParser { 873 @Override public int parsePart(String value, EventRecurrence er) { 874 int[] bysetpos = parseNumberList(value, Integer.MIN_VALUE, Integer.MAX_VALUE, true); 875 er.bysetpos = bysetpos; 876 er.bysetposCount = bysetpos.length; 877 return PARSED_BYSETPOS; 878 } 879 } 880 /** parses WKST={SU,MO,...} */ 881 private static class ParseWkst extends PartParser { 882 @Override public int parsePart(String value, EventRecurrence er) { 883 Integer wkst = sParseWeekdayMap.get(value); 884 if (wkst == null) { 885 throw new InvalidFormatException("Invalid WKST value: " + value); 886 } 887 er.wkst = wkst; 888 return PARSED_WKST; 889 } 890 } 891 } 892