1 | // jademx - JADE management using JMX |
2 | // Copyright 2004-2005 Caboodle Networks, Inc. |
3 | // |
4 | // This library is free software; you can redistribute it and/or |
5 | // modify it under the terms of the GNU Lesser General Public |
6 | // License as published by the Free Software Foundation; either |
7 | // version 2.1 of the License, or (at your option) any later version. |
8 | // |
9 | // This library is distributed in the hope that it will be useful, |
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
12 | // Lesser General Public License for more details. |
13 | // |
14 | // You should have received a copy of the GNU Lesser General Public |
15 | // License along with this library; if not, write to the Free Software |
16 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
17 | |
18 | package jade.jademx.util.iso8601; |
19 | |
20 | import java.io.Serializable; |
21 | import java.text.DecimalFormatSymbols; |
22 | import java.util.Locale; |
23 | import java.math.BigDecimal; |
24 | import java.math.BigInteger; |
25 | |
26 | /** |
27 | * Represent an ISO 8601 duration. |
28 | * Compliant with ISO 8601:2000 section 5.5.4.2. |
29 | * Note that the setting of weeks is mutually exclusive from setting of |
30 | * all other time fields. |
31 | * !!!TODO allow non-second fractional parts |
32 | * @author David Bernstein, <a href="http://www.caboodlenetworks.com" |
33 | * >Caboodle Networks, Inc.</a> |
34 | */ |
35 | public class Duration implements Serializable, Cloneable { |
36 | |
37 | /** is duration expressed in weeks or not */ |
38 | private boolean isWeeks = false; |
39 | |
40 | /** is extended format used */ |
41 | private boolean extended = false; |
42 | |
43 | /** is alternative format used */ |
44 | private boolean alternative = false; |
45 | |
46 | /** years */ |
47 | private int years = 0; |
48 | |
49 | /** months */ |
50 | private int months = 0; |
51 | |
52 | /** days */ |
53 | private int days = 0; |
54 | |
55 | /** hours */ |
56 | private int hours = 0; |
57 | |
58 | /** minutes */ |
59 | private int minutes = 0; |
60 | |
61 | /** seconds */ |
62 | private int seconds = 0; |
63 | |
64 | /** weeks */ |
65 | private int weeks = 0; |
66 | |
67 | /** locale */ |
68 | private Locale locale = null; |
69 | |
70 | /** unit fraction applies to */ |
71 | private char fractionUnit = UNSET_FRACTION; |
72 | |
73 | /** fraction */ |
74 | private String fraction = null; |
75 | |
76 | // put format constants in ISO8601.java and allow setting here to govern toString() |
77 | |
78 | // fraction constant are different from designators because |
79 | // months and minutes would collide |
80 | |
81 | /** year fraction */ |
82 | public final static char YEAR_FRACTION = 'Y'; |
83 | |
84 | /** month fraction */ |
85 | public final static char MONTH_FRACTION = 'M'; |
86 | |
87 | /** day fraction */ |
88 | public final static char DAY_FRACTION = 'D'; |
89 | |
90 | /** hour fraction */ |
91 | public final static char HOUR_FRACTION = 'H'; |
92 | |
93 | /** minute fraction */ |
94 | public final static char MINUTE_FRACTION = 'm'; |
95 | |
96 | /** second fraction */ |
97 | public final static char SECOND_FRACTION = 'S'; |
98 | |
99 | /** week fraction */ |
100 | public final static char WEEK_FRACTION = 'W'; |
101 | |
102 | /** unset fraction */ |
103 | public final static char UNSET_FRACTION = 'T'; |
104 | |
105 | // move to ISO8601.java |
106 | |
107 | /** duration designator */ |
108 | private final static char DURATION_DESIGNATOR = 'P'; |
109 | |
110 | /** year designator */ |
111 | private final static char YEAR_DESIGNATOR = 'Y'; |
112 | |
113 | /** month designator */ |
114 | private final static char MONTH_DESIGNATOR = 'M'; |
115 | |
116 | /** day designator */ |
117 | private final static char DAY_DESIGNATOR = 'D'; |
118 | |
119 | /** hour designator */ |
120 | private final static char HOUR_DESIGNATOR = 'H'; |
121 | |
122 | /** minute designator */ |
123 | private final static char MINUTE_DESIGNATOR = 'M'; |
124 | |
125 | /** second designator */ |
126 | private final static char SECOND_DESIGNATOR = 'S'; |
127 | |
128 | /** week designator */ |
129 | private final static char WEEK_DESIGNATOR = 'W'; |
130 | |
131 | /** time designator */ |
132 | private final static char TIME_DESIGNATOR = 'T'; |
133 | |
134 | /** full stop character */ |
135 | private final static char FULL_STOP = '.'; |
136 | |
137 | /** comma character */ |
138 | private final static char COMMA = ','; |
139 | |
140 | /** hyphen character */ |
141 | private final static char HYPHEN = '-'; |
142 | |
143 | /** colon character */ |
144 | private final static char COLON = ':'; |
145 | |
146 | /** number of milliseconds in a second */ |
147 | private final static int MS_IN_SEC = 1000; |
148 | |
149 | /** number of seconds in a minute */ |
150 | private final static int SEC_IN_MIN = 60; |
151 | |
152 | /** number of minutes in an hour */ |
153 | private final static int MIN_IN_HR = 60; |
154 | |
155 | /** number of hours in a day */ |
156 | private final static int HR_IN_DAY = 24; |
157 | |
158 | /** number of days in week */ |
159 | private final static int DAY_IN_WK = 7; |
160 | |
161 | /** number of days in month - ARBITRARY */ |
162 | private final static int DAY_IN_MON = 30; |
163 | |
164 | /** number of days in year - ARBITRARY */ |
165 | private final static int DAY_IN_YR = 365; |
166 | |
167 | /** |
168 | * make an ISO 8601 duration object |
169 | */ |
170 | public Duration() { |
171 | } |
172 | |
173 | /** |
174 | * return whether given character is an ASCII digit |
175 | * @param c character to test for digitness |
176 | * @return whether given character is an ASCII digit |
177 | */ |
178 | private boolean isAsciiDigit( char c ) { |
179 | return ( c >= '0' ) && ( c <= '9' ); |
180 | } |
181 | |
182 | /** |
183 | * return |
184 | * @param s string to look at |
185 | * @param start index to start at |
186 | * @return index after start of first non-digit character (may be length of string) |
187 | */ |
188 | private int nonDigitIndex( String s, int start ) { |
189 | if ( !isAsciiDigit( s.charAt( start ) ) ) { |
190 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
191 | "expected digit at position " + start + |
192 | " but found '" + s.charAt( start ) + "'" ); |
193 | } |
194 | int sLen = s.length(); |
195 | int firstNonDigitIndex = sLen; |
196 | for ( int i = start+1; i < sLen; i++ ) { |
197 | if ( !isAsciiDigit( s.charAt( i ) ) ) { |
198 | firstNonDigitIndex = i; |
199 | break; |
200 | } |
201 | } |
202 | return firstNonDigitIndex; |
203 | } |
204 | |
205 | /** |
206 | * helper method to parse() to get just the HMS part. |
207 | * @param s string representation |
208 | * @param tIndex where the 'T' was found |
209 | */ |
210 | private void parseHMS( String s, int tIndex ) { |
211 | int sLen = s.length(); |
212 | int firstDelimIndex = nonDigitIndex( s, tIndex+1 ); |
213 | if ( firstDelimIndex >= sLen ) { |
214 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
215 | "no fields specified" ); |
216 | } |
217 | char firstDelimChar = s.charAt( firstDelimIndex ); |
218 | int thisInt; |
219 | try { |
220 | thisInt = Integer.parseInt( s.substring( tIndex+1, firstDelimIndex ) ); |
221 | } |
222 | catch ( Exception e ) { |
223 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
224 | "unable to parse integer substring" ); |
225 | } |
226 | String thisFraction = null; |
227 | int fractionStart; |
228 | if ( ( firstDelimChar == FULL_STOP ) || |
229 | ( firstDelimChar == COMMA ) ) { |
230 | if ( fractionUnit != UNSET_FRACTION ) { |
231 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
232 | "only one fractional part allowed" ); |
233 | } |
234 | fractionStart = firstDelimIndex+1; |
235 | firstDelimIndex = nonDigitIndex( s, firstDelimIndex+1 ); |
236 | thisFraction = s.substring( fractionStart, firstDelimIndex ); |
237 | firstDelimChar = s.charAt( firstDelimIndex ); |
238 | } |
239 | switch ( firstDelimChar ) { |
240 | case HOUR_DESIGNATOR: |
241 | hours = thisInt; |
242 | if ( null != thisFraction ) { |
243 | setFractionInternal( HOUR_FRACTION, |
244 | thisFraction ); |
245 | thisFraction = null; |
246 | } |
247 | break; |
248 | case MINUTE_DESIGNATOR: |
249 | minutes= thisInt; |
250 | if ( null != thisFraction ) { |
251 | setFractionInternal( MINUTE_FRACTION, |
252 | thisFraction ); |
253 | thisFraction = null; |
254 | } |
255 | break; |
256 | case SECOND_DESIGNATOR: |
257 | seconds = thisInt; |
258 | if ( null != thisFraction ) { |
259 | setFractionInternal( SECOND_FRACTION, |
260 | thisFraction ); |
261 | thisFraction = null; |
262 | } |
263 | break; |
264 | default: |
265 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
266 | "'" + firstDelimChar + |
267 | "' found where '" + |
268 | HOUR_DESIGNATOR + "', '" + |
269 | MINUTE_DESIGNATOR + "', or '" + |
270 | SECOND_DESIGNATOR + "' expected" ); |
271 | } |
272 | if ( sLen > ( firstDelimIndex+1 ) ) { |
273 | char lastUnitSeen = firstDelimChar; |
274 | int secondDelimIndex = nonDigitIndex( s, firstDelimIndex+1 ); |
275 | if ( secondDelimIndex < sLen ) { |
276 | char secondDelimChar = s.charAt( secondDelimIndex ); |
277 | try { |
278 | thisInt = Integer.parseInt( s.substring( firstDelimIndex+1, |
279 | secondDelimIndex ) ); |
280 | } |
281 | catch ( Exception e ) { |
282 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
283 | "unable to parse integer substring" ); |
284 | } |
285 | if ( ( secondDelimChar == FULL_STOP ) || |
286 | ( secondDelimChar == COMMA ) ) { |
287 | if ( fractionUnit != UNSET_FRACTION ) { |
288 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
289 | "only one fractional part allowed" ); |
290 | } |
291 | fractionStart = secondDelimIndex+1; |
292 | secondDelimIndex = nonDigitIndex( s, secondDelimIndex+1 ); |
293 | thisFraction = s.substring( fractionStart, secondDelimIndex ); |
294 | secondDelimChar = s.charAt( secondDelimIndex ); |
295 | } |
296 | switch ( secondDelimChar ) { |
297 | case MINUTE_DESIGNATOR: |
298 | if ( ( MINUTE_DESIGNATOR == lastUnitSeen ) || |
299 | ( SECOND_DESIGNATOR == lastUnitSeen ) ) { |
300 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
301 | "'" + secondDelimChar + |
302 | "' follows '" + |
303 | firstDelimChar + "'" ); |
304 | } |
305 | minutes = thisInt; |
306 | if ( null != thisFraction ) { |
307 | setFractionInternal( MINUTE_FRACTION, |
308 | thisFraction ); |
309 | thisFraction = null; |
310 | } |
311 | break; |
312 | case SECOND_DESIGNATOR: |
313 | if ( SECOND_DESIGNATOR == lastUnitSeen ) { |
314 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
315 | "'" + secondDelimChar + |
316 | "' follows '" + |
317 | firstDelimChar + "'" ); |
318 | } |
319 | seconds = thisInt; |
320 | if ( null != thisFraction ) { |
321 | setFractionInternal( SECOND_FRACTION, |
322 | thisFraction ); |
323 | thisFraction = null; |
324 | } |
325 | break; |
326 | default: |
327 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
328 | "'" + secondDelimChar + |
329 | "' found where '" + |
330 | MINUTE_DESIGNATOR + "' or '" + |
331 | SECOND_DESIGNATOR + "' expected" ); |
332 | } |
333 | if ( sLen > ( secondDelimIndex+1 ) ) { |
334 | lastUnitSeen = secondDelimChar; |
335 | int thirdDelimIndex = nonDigitIndex( s, secondDelimIndex+1 ); |
336 | if ( thirdDelimIndex < sLen ) { |
337 | char thirdDelimChar = s.charAt( thirdDelimIndex ); |
338 | try { |
339 | thisInt = Integer.parseInt( s.substring( secondDelimIndex+1, |
340 | thirdDelimIndex ) ); |
341 | } |
342 | catch ( Exception e ) { |
343 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
344 | "unable to parse integer substring" ); |
345 | } |
346 | thisFraction = null; |
347 | if ( ( thirdDelimChar == FULL_STOP ) || |
348 | ( thirdDelimChar == COMMA ) ) { |
349 | if ( fractionUnit != UNSET_FRACTION ) { |
350 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
351 | "only one fractional part allowed" ); |
352 | } |
353 | fractionStart = thirdDelimIndex+1; |
354 | thirdDelimIndex = nonDigitIndex( s, thirdDelimIndex+1 ); |
355 | thisFraction = s.substring( fractionStart, thirdDelimIndex ); |
356 | thirdDelimChar = s.charAt( thirdDelimIndex ); |
357 | } |
358 | switch ( thirdDelimChar ) { |
359 | case SECOND_DESIGNATOR: |
360 | if ( SECOND_DESIGNATOR == lastUnitSeen ) { |
361 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
362 | "'" + thirdDelimChar + |
363 | "' follows '" + |
364 | secondDelimChar + "'" ); |
365 | } |
366 | seconds = thisInt; |
367 | if ( null != thisFraction ) { |
368 | setFractionInternal( SECOND_FRACTION, |
369 | thisFraction ); |
370 | } |
371 | break; |
372 | default: |
373 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
374 | "'" + thirdDelimChar + |
375 | "' found where '" + |
376 | SECOND_DESIGNATOR + "' expected" ); |
377 | } |
378 | if ( sLen > ( thirdDelimIndex+1 ) ) { |
379 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
380 | "extra unexpected characters found" ); |
381 | } |
382 | } |
383 | } |
384 | } |
385 | } |
386 | |
387 | } |
388 | |
389 | /** |
390 | * set fields according to string representation |
391 | * @param s string representation |
392 | */ |
393 | private void parse( String s ) { |
394 | |
395 | fraction = null; |
396 | fractionUnit = UNSET_FRACTION; |
397 | |
398 | if ( s.charAt(0) != DURATION_DESIGNATOR ) { |
399 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
400 | "first character of ISO 8601 duration is not '"+ |
401 | DURATION_DESIGNATOR + "'" ); |
402 | } |
403 | if ( s.length() == 1 ) { |
404 | throw new IllegalArgumentException( "no fields in string \"" + s + "\"" ); |
405 | } |
406 | |
407 | // if next char is time designator |
408 | // format is primary with values < day |
409 | // else |
410 | // get following integer string (parse exception if not) |
411 | // if character after that is decimal sign (full stop or comma) |
412 | // get next integer string as fraction |
413 | // if character after parsed integer strings is week designator |
414 | // format is PnW (primary/weeks) (fraction ok) |
415 | // elsif int string is 4 digits and followed by hyphen |
416 | // format is PYYYY-MM-DDThh:mm:ss (alternative/extended) (only seconds can have fraction) |
417 | // elsif int string is 8 digits and followed by time designator |
418 | // format is PYYYYMMDDThhmmss (alternative/basic) (only seconds can have fraction) |
419 | // else |
420 | // format must be primary: PnYnMnDTnHnMnS |
421 | // where zero values may be omitted and fraction can only occur |
422 | // in final field integer string and T must be used only if hours, |
423 | // minutes, and/or seconds are specified. |
424 | |
425 | int sLen = s.length(); |
426 | |
427 | if ( s.charAt(1) == TIME_DESIGNATOR ) { |
428 | // PTnHnMnS (primary - no year/month/day) |
429 | isWeeks = false; |
430 | extended = false; |
431 | alternative = false; |
432 | parseHMS( s, 1 ); |
433 | } |
434 | else { |
435 | int firstDelimIndex = nonDigitIndex( s, 1 ); |
436 | if ( firstDelimIndex >= sLen ) { |
437 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
438 | "no fields specified" ); |
439 | } |
440 | char firstDelimChar = s.charAt( firstDelimIndex ); |
441 | if ( ( FULL_STOP == firstDelimChar ) || |
442 | ( COMMA == firstDelimChar ) ) { |
443 | throw new RuntimeException("fractions not yet implemented"); |
444 | // be sure to set firstDelimChar to char *after* fractional part |
445 | } |
446 | if ( WEEK_DESIGNATOR == firstDelimChar ) { |
447 | // PnW |
448 | isWeeks = true; |
449 | alternative = false; |
450 | // extended not applicable |
451 | throw new RuntimeException("week parsing yet implemented"); |
452 | } |
453 | else if ( ( 5 == firstDelimIndex ) && ( HYPHEN == firstDelimChar ) ) { |
454 | // PYYYY-MM-DDThh:mm:ss (alternative/extended) |
455 | isWeeks = false; |
456 | alternative = true; |
457 | extended = true; |
458 | throw new RuntimeException("alternative extended parsing yet implemented"); |
459 | } |
460 | else if ( ( 9 == firstDelimIndex ) && ( TIME_DESIGNATOR == firstDelimChar ) ) { |
461 | // PYYYYMMDDThhmmss (alternative/basic) |
462 | isWeeks = false; |
463 | alternative = true; |
464 | extended = false; |
465 | throw new RuntimeException("alternative basic parsing yet implemented"); |
466 | } |
467 | else { |
468 | // PnYnMnDTnHnMnS (primary) |
469 | isWeeks = false; |
470 | alternative = false; |
471 | // extended not applicable |
472 | int tIndex = -1; // where 'T' seen |
473 | // get YMD, then T delim if HMS, then HMS |
474 | int thisInt; |
475 | try { |
476 | thisInt = Integer.parseInt( s.substring( 1, firstDelimIndex ) ); |
477 | } |
478 | catch ( Exception e ) { |
479 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
480 | "unable to parse integer substring" ); |
481 | } |
482 | switch ( firstDelimChar ) { |
483 | case YEAR_DESIGNATOR: |
484 | years = thisInt; |
485 | break; |
486 | case MONTH_DESIGNATOR: |
487 | months = thisInt; |
488 | break; |
489 | case DAY_DESIGNATOR: |
490 | days = thisInt; |
491 | break; |
492 | default: |
493 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
494 | "'" + firstDelimChar + |
495 | "' found where '" + |
496 | YEAR_DESIGNATOR + "', '" + |
497 | MONTH_DESIGNATOR + "', or '" + |
498 | DAY_DESIGNATOR + "' expected" ); |
499 | } |
500 | if ( sLen > ( firstDelimIndex+1 ) ) { |
501 | if ( TIME_DESIGNATOR == s.charAt( firstDelimIndex+1 ) ) { |
502 | tIndex = firstDelimIndex + 1; |
503 | } |
504 | else { |
505 | char lastUnitSeen = firstDelimChar; |
506 | int secondDelimIndex = nonDigitIndex( s, firstDelimIndex+1 ); |
507 | if ( secondDelimIndex < sLen ) { |
508 | char secondDelimChar = s.charAt( secondDelimIndex ); |
509 | try { |
510 | thisInt = Integer.parseInt( s.substring( firstDelimIndex+1, |
511 | secondDelimIndex ) ); |
512 | } |
513 | catch ( Exception e ) { |
514 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
515 | "unable to parse integer substring" ); |
516 | } |
517 | switch ( secondDelimChar ) { |
518 | case MONTH_DESIGNATOR: |
519 | if ( ( DAY_DESIGNATOR == lastUnitSeen ) || |
520 | ( MONTH_DESIGNATOR == lastUnitSeen ) ) { |
521 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
522 | "'" + secondDelimChar + |
523 | "' follows '" + |
524 | firstDelimChar + "'" ); |
525 | } |
526 | months = thisInt; |
527 | break; |
528 | case DAY_DESIGNATOR: |
529 | if ( DAY_DESIGNATOR == lastUnitSeen ) { |
530 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
531 | "'" + secondDelimChar + |
532 | "' follows '" + |
533 | firstDelimChar + "'" ); |
534 | } |
535 | days = thisInt; |
536 | break; |
537 | default: |
538 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
539 | "'" + secondDelimChar + |
540 | "' found where '" + |
541 | MONTH_DESIGNATOR + "' or '" + |
542 | DAY_DESIGNATOR + "' expected" ); |
543 | } |
544 | |
545 | if ( sLen > ( secondDelimIndex+1 ) ) { |
546 | if ( TIME_DESIGNATOR == s.charAt( secondDelimIndex+1 ) ) { |
547 | tIndex = secondDelimIndex + 1; |
548 | } |
549 | else { |
550 | lastUnitSeen = secondDelimChar; |
551 | int thirdDelimIndex = nonDigitIndex( s, secondDelimIndex+1 ); |
552 | if ( thirdDelimIndex < sLen ) { |
553 | char thirdDelimChar = s.charAt( thirdDelimIndex ); |
554 | try { |
555 | thisInt = Integer.parseInt( s.substring( secondDelimIndex+1, |
556 | thirdDelimIndex ) ); |
557 | } |
558 | catch ( Exception e ) { |
559 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
560 | "unable to parse integer substring" ); |
561 | } |
562 | switch ( thirdDelimChar ) { |
563 | case DAY_DESIGNATOR: |
564 | if ( DAY_DESIGNATOR == lastUnitSeen ) { |
565 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
566 | "'" + thirdDelimChar + |
567 | "' follows '" + |
568 | secondDelimChar + "'" ); |
569 | } |
570 | days = thisInt; |
571 | lastUnitSeen = thirdDelimChar; |
572 | break; |
573 | default: |
574 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
575 | "'" + thirdDelimChar + |
576 | "' found where '" + |
577 | DAY_DESIGNATOR + "' or '" + |
578 | TIME_DESIGNATOR + "' expected" ); |
579 | } |
580 | } |
581 | if ( sLen > ( thirdDelimIndex+1 ) ) { |
582 | if ( TIME_DESIGNATOR == s.charAt( thirdDelimIndex+1 ) ) { |
583 | tIndex = thirdDelimIndex + 1; |
584 | } |
585 | else { |
586 | throw new IllegalArgumentException( "in string \"" + s + "\":" + |
587 | "only '" + TIME_DESIGNATOR + |
588 | "' expected after '" + |
589 | lastUnitSeen + "'" ); |
590 | } |
591 | } |
592 | } |
593 | } |
594 | |
595 | } |
596 | |
597 | } |
598 | } |
599 | if ( tIndex > 0 ) { |
600 | parseHMS( s, tIndex ); |
601 | } |
602 | } |
603 | } |
604 | |
605 | } |
606 | |
607 | /** |
608 | * make an ISO 8601 duration object |
609 | * @param s string representation |
610 | */ |
611 | public Duration( String s ) { |
612 | parse( s ); |
613 | } |
614 | |
615 | /** |
616 | * set years |
617 | * @param years years |
618 | */ |
619 | public void setYears( int years ) { |
620 | isWeeks = false; |
621 | this.years = years; |
622 | } |
623 | |
624 | /** |
625 | * get years |
626 | * @return years |
627 | */ |
628 | public int getYears() { |
629 | return years; |
630 | } |
631 | |
632 | /** |
633 | * set months |
634 | * @param months months |
635 | */ |
636 | public void setMonths( int months ) { |
637 | isWeeks = false; |
638 | this.months = months; |
639 | } |
640 | |
641 | /** |
642 | * get months |
643 | * @return months |
644 | */ |
645 | public int getMonths() { |
646 | return months; |
647 | } |
648 | |
649 | /** |
650 | * set days |
651 | * @param days days |
652 | */ |
653 | public void setDays( int days ) { |
654 | isWeeks = false; |
655 | this.days = days; |
656 | } |
657 | |
658 | /** |
659 | * get days |
660 | * @return days |
661 | */ |
662 | public int getDays() { |
663 | return days; |
664 | } |
665 | |
666 | /** |
667 | * set hours |
668 | * @param hours hours |
669 | */ |
670 | public void setHours( int hours ) { |
671 | isWeeks = false; |
672 | this.hours = hours; |
673 | } |
674 | |
675 | /** |
676 | * get hours |
677 | * @return hours |
678 | */ |
679 | public int getHours() { |
680 | return hours; |
681 | } |
682 | |
683 | /** |
684 | * set minutes |
685 | * @param minutes minutes |
686 | */ |
687 | public void setMinutes( int minutes ) { |
688 | isWeeks = false; |
689 | this.minutes = minutes; |
690 | } |
691 | |
692 | /** |
693 | * get minutes |
694 | * @return minutes |
695 | */ |
696 | public int getMinutes() { |
697 | return minutes; |
698 | } |
699 | |
700 | /** |
701 | * set seconds |
702 | * @param seconds seconds |
703 | */ |
704 | public void setSeconds( int seconds ) { |
705 | isWeeks = false; |
706 | this.seconds = seconds; |
707 | } |
708 | |
709 | /** |
710 | * get seconds |
711 | * @return seconds |
712 | */ |
713 | public int getSeconds() { |
714 | return seconds; |
715 | } |
716 | |
717 | /** |
718 | * set weeks |
719 | * @param weeks weeks |
720 | */ |
721 | public void setWeeks( int weeks ) { |
722 | if ( alternative ) { |
723 | throw new RuntimeException("weeks are incompatible with alternative format"); |
724 | } |
725 | isWeeks = true; |
726 | this.weeks = weeks; |
727 | } |
728 | |
729 | /** |
730 | * get weeks |
731 | * @return weeks |
732 | */ |
733 | public int getWeeks() { |
734 | return weeks; |
735 | } |
736 | |
737 | /** |
738 | * return whether using weeks or other |
739 | * @return whether using weeks or other |
740 | */ |
741 | public boolean isWeeks() { |
742 | return isWeeks; |
743 | } |
744 | |
745 | /** |
746 | * set locale |
747 | * @param locale locale |
748 | */ |
749 | public void setLocale( Locale locale ) { |
750 | this.locale = locale; |
751 | } |
752 | |
753 | /** |
754 | * get locale |
755 | * @return locale |
756 | */ |
757 | public Locale getLocale() { |
758 | return locale; |
759 | } |
760 | |
761 | /** |
762 | * set fraction. Until public fraction setting supported |
763 | * fraction can only be set for least significant used unit. |
764 | * for example, specify fraction for minutes and then specifying nonzero |
765 | * for seconds is illegal. |
766 | * @param fractionUnit which unit fraction is for |
767 | * @param fraction fraction |
768 | */ |
769 | private void setFractionInternal( char fractionUnit, String fraction ) { |
770 | |
771 | switch ( fractionUnit ) { |
772 | case YEAR_FRACTION: |
773 | case MONTH_FRACTION: |
774 | case WEEK_FRACTION: |
775 | case DAY_FRACTION: |
776 | case HOUR_FRACTION: |
777 | case MINUTE_FRACTION: |
778 | case SECOND_FRACTION: |
779 | // OK: NOP |
780 | break; |
781 | default: |
782 | throw new IllegalArgumentException("illegal fraction unit '"+ |
783 | fractionUnit + "'"); |
784 | } |
785 | |
786 | int fractionLen = fraction.length(); |
787 | |
788 | // check for non-digit characters |
789 | for ( int i = 0; i < fractionLen; i++ ) { |
790 | if ( !isAsciiDigit( fraction.charAt( i ) ) ) { |
791 | throw new IllegalArgumentException( "expected digit at position " + i + |
792 | " of fraction but found '" + |
793 | fraction.charAt( i ) + "'" ); |
794 | } |
795 | } |
796 | |
797 | // truncate trailing zeroes |
798 | StringBuffer sb = new StringBuffer( fraction ); |
799 | for ( int i = fraction.length()-1; i >= 0; i-- ) { |
800 | if ( ( sb.length() == i ) && |
801 | ( fraction.charAt( i ) == '0' ) ) { |
802 | sb.setLength( i ); |
803 | } |
804 | else { |
805 | break; |
806 | } |
807 | } |
808 | |
809 | if ( 0 == sb.length() ) { |
810 | throw new IllegalArgumentException( "zero or empty fraction" ); |
811 | } |
812 | |
813 | this.fractionUnit = fractionUnit; |
814 | this.fraction = sb.toString(); |
815 | |
816 | } |
817 | |
818 | /** |
819 | * set fraction. |
820 | * fraction can only be set for least significant used unit. |
821 | * for example, specify fraction for minutes and then specifying nonzero |
822 | * for seconds is illegal. |
823 | * @param fractionUnit which unit fraction is for |
824 | * @param fraction fraction |
825 | */ |
826 | public void setFraction( char fractionUnit, String fraction ) { |
827 | // need to validate only digits |
828 | // need to normalize: truncate trailing zeros - eliminate if all zeros |
829 | this.fraction = fraction; |
830 | throw new RuntimeException(Duration.class.getName()+".setFraction(): fractions not yet implemented"); |
831 | } |
832 | |
833 | /** |
834 | * get fraction |
835 | * @return fraction (null if unset) |
836 | */ |
837 | public String getFraction() { |
838 | return fraction; |
839 | } |
840 | |
841 | /** |
842 | * get fraction unit |
843 | * @return fraction unit ('T' if unset) |
844 | */ |
845 | public char getFractionUnit() { |
846 | return fractionUnit; |
847 | } |
848 | |
849 | /** |
850 | * set extended format |
851 | * @param extended extended format |
852 | */ |
853 | public void setExtended( boolean extended ) { |
854 | this.extended = extended; |
855 | } |
856 | |
857 | /** |
858 | * get whether extended format |
859 | * @return whether extended |
860 | */ |
861 | public boolean isExtended() { |
862 | return extended; |
863 | } |
864 | |
865 | /** |
866 | * set alternative format |
867 | * @param alternative alternative format |
868 | */ |
869 | public void setAlternative( boolean alternative ) { |
870 | if ( alternative && isWeeks ) { |
871 | throw new RuntimeException("alternative format is incompatible with weeks"); |
872 | } |
873 | this.alternative = alternative; |
874 | } |
875 | |
876 | /** |
877 | * get whether alternative format |
878 | * @return whether alternative |
879 | */ |
880 | public boolean isAlternative() { |
881 | return alternative; |
882 | } |
883 | |
884 | /** |
885 | * return the number of seconds this duration represents. |
886 | * rounded to nearest second if necessary. |
887 | * month have 30 days and years have 365 days. |
888 | * @return the number of seconds this duration represents |
889 | */ |
890 | public long toSeconds() { |
891 | long inSeconds; |
892 | if ( isWeeks ) { |
893 | inSeconds = |
894 | ( weeks * ( DAY_IN_WK * HR_IN_DAY * MIN_IN_HR * SEC_IN_MIN ) ); |
895 | } |
896 | else { |
897 | inSeconds = |
898 | ( years * ( DAY_IN_YR * HR_IN_DAY * MIN_IN_HR * SEC_IN_MIN ) ) + |
899 | ( months * ( DAY_IN_MON * HR_IN_DAY * MIN_IN_HR * SEC_IN_MIN ) ) + |
900 | ( days * ( HR_IN_DAY * MIN_IN_HR * SEC_IN_MIN ) ) + |
901 | ( hours * ( MIN_IN_HR * SEC_IN_MIN ) ) + |
902 | ( minutes * ( SEC_IN_MIN ) ) + |
903 | seconds; |
904 | } |
905 | if ( UNSET_FRACTION != fractionUnit ) { |
906 | String fractionString = "0." + fraction; |
907 | BigDecimal decimalFraction = new BigDecimal( fractionString ); |
908 | BigDecimal unitMultiplier; |
909 | BigDecimal fractionsPart; |
910 | switch ( fractionUnit ) { |
911 | case YEAR_FRACTION: |
912 | unitMultiplier = |
913 | new BigDecimal( BigInteger.valueOf( DAY_IN_YR * HR_IN_DAY * MIN_IN_HR * SEC_IN_MIN ) ); |
914 | break; |
915 | case MONTH_FRACTION: |
916 | unitMultiplier = |
917 | new BigDecimal( BigInteger.valueOf( DAY_IN_MON * HR_IN_DAY * MIN_IN_HR * SEC_IN_MIN ) ); |
918 | break; |
919 | case WEEK_FRACTION: |
920 | unitMultiplier = |
921 | new BigDecimal( BigInteger.valueOf( DAY_IN_WK * HR_IN_DAY * MIN_IN_HR * SEC_IN_MIN ) ); |
922 | break; |
923 | case DAY_FRACTION: |
924 | unitMultiplier = |
925 | new BigDecimal( BigInteger.valueOf( HR_IN_DAY * MIN_IN_HR * SEC_IN_MIN ) ); |
926 | break; |
927 | case HOUR_FRACTION: |
928 | unitMultiplier = |
929 | new BigDecimal( BigInteger.valueOf( MIN_IN_HR * SEC_IN_MIN ) ); |
930 | break; |
931 | case MINUTE_FRACTION: |
932 | unitMultiplier = |
933 | new BigDecimal( BigInteger.valueOf( SEC_IN_MIN ) ); |
934 | break; |
935 | case SECOND_FRACTION: |
936 | unitMultiplier = |
937 | new BigDecimal( BigInteger.valueOf( 1 ) ); |
938 | break; |
939 | default: |
940 | throw new RuntimeException("unreachable"); |
941 | } |
942 | fractionsPart = decimalFraction.multiply( unitMultiplier ); |
943 | inSeconds += fractionsPart.longValue(); |
944 | } |
945 | return inSeconds; |
946 | } |
947 | |
948 | /** |
949 | * return the number of milliseconds this duration represents |
950 | * rounded to nearest millisecond if necessary. |
951 | * month have 30 days and years have 365 days. |
952 | * @return the number of milliseconds this duration represents |
953 | */ |
954 | public long toMilliseconds() { |
955 | // code is duplicated rather than called from toSeconds() to avoid rounding error |
956 | long inMilliseconds; |
957 | if ( isWeeks ) { |
958 | inMilliseconds = |
959 | ( weeks * ( DAY_IN_WK * HR_IN_DAY * MIN_IN_HR * SEC_IN_MIN * MS_IN_SEC ) ); |
960 | } |
961 | else { |
962 | inMilliseconds = |
963 | ( years * ( DAY_IN_YR * HR_IN_DAY * MIN_IN_HR * SEC_IN_MIN * MS_IN_SEC ) ) + |
964 | ( months * ( DAY_IN_MON * HR_IN_DAY * MIN_IN_HR * SEC_IN_MIN * MS_IN_SEC ) ) + |
965 | ( days * ( HR_IN_DAY * MIN_IN_HR * SEC_IN_MIN * MS_IN_SEC ) ) + |
966 | ( hours * ( MIN_IN_HR * SEC_IN_MIN * MS_IN_SEC ) ) + |
967 | ( minutes * ( SEC_IN_MIN * MS_IN_SEC ) ) + |
968 | ( seconds * ( MS_IN_SEC ) ); |
969 | } |
970 | if ( UNSET_FRACTION != fractionUnit ) { |
971 | if ( SECOND_FRACTION == fractionUnit ) { |
972 | // can cheaply and easily and accurately compute case where fraction |
973 | // is in seconds |
974 | String fractionString = fraction; |
975 | boolean roundUpMs = false; |
976 | int fractionLen = fraction.length(); |
977 | if ( fractionLen > 3 ) { |
978 | fractionString = fraction.substring( 0, 3 ); |
979 | // check for needing to round up |
980 | char c = fraction.charAt( 3 ); |
981 | if ( ( c >= '5' ) && ( c <= '9' ) ) { |
982 | roundUpMs = true; |
983 | } |
984 | } |
985 | else if ( fractionLen == 3 ) { |
986 | fractionString = fraction; |
987 | } |
988 | else { |
989 | StringBuffer fractionSB = new StringBuffer( fraction ); |
990 | while ( fractionSB.length() < 3 ) { |
991 | fractionSB.append( '0' ); |
992 | } |
993 | fractionString = fractionSB.toString(); |
994 | } |
995 | int msInFraction = 100 * ( fractionString.charAt(0) - '0' ); |
996 | msInFraction += 10 * ( fractionString.charAt(1) - '0' ); |
997 | msInFraction += fractionString.charAt(2) - '0' ; |
998 | if ( roundUpMs ) { |
999 | msInFraction++; |
1000 | } |
1001 | inMilliseconds += msInFraction; |
1002 | } |
1003 | else { |
1004 | BigDecimal decimalFraction = new BigDecimal( "0." + fraction ); |
1005 | BigDecimal unitMultiplier; |
1006 | BigDecimal fractionsPart; |
1007 | switch ( fractionUnit ) { |
1008 | case YEAR_FRACTION: |
1009 | unitMultiplier = |
1010 | new BigDecimal( BigInteger.valueOf( DAY_IN_YR * HR_IN_DAY * MIN_IN_HR * SEC_IN_MIN * MS_IN_SEC ) ); |
1011 | break; |
1012 | case MONTH_FRACTION: |
1013 | unitMultiplier = |
1014 | new BigDecimal( BigInteger.valueOf( DAY_IN_MON * HR_IN_DAY * MIN_IN_HR * SEC_IN_MIN * MS_IN_SEC ) ); |
1015 | break; |
1016 | case WEEK_FRACTION: |
1017 | unitMultiplier = |
1018 | new BigDecimal( BigInteger.valueOf( DAY_IN_WK * HR_IN_DAY * MIN_IN_HR * SEC_IN_MIN * MS_IN_SEC ) ); |
1019 | break; |
1020 | case DAY_FRACTION: |
1021 | unitMultiplier = |
1022 | new BigDecimal( BigInteger.valueOf( HR_IN_DAY * MIN_IN_HR * SEC_IN_MIN * MS_IN_SEC ) ); |
1023 | break; |
1024 | case HOUR_FRACTION: |
1025 | unitMultiplier = |
1026 | new BigDecimal( BigInteger.valueOf( MIN_IN_HR * SEC_IN_MIN * MS_IN_SEC ) ); |
1027 | break; |
1028 | case MINUTE_FRACTION: |
1029 | unitMultiplier = |
1030 | new BigDecimal( BigInteger.valueOf( SEC_IN_MIN * MS_IN_SEC ) ); |
1031 | break; |
1032 | case SECOND_FRACTION: |
1033 | unitMultiplier = |
1034 | new BigDecimal( BigInteger.valueOf( MS_IN_SEC ) ); |
1035 | break; |
1036 | default: |
1037 | throw new RuntimeException("unreachable"); |
1038 | } |
1039 | fractionsPart = decimalFraction.multiply( unitMultiplier ); |
1040 | inMilliseconds += fractionsPart.longValue(); |
1041 | } |
1042 | } |
1043 | return inMilliseconds; |
1044 | } |
1045 | |
1046 | /** |
1047 | * convert integer to zero-padded string |
1048 | * @param i integer to convert |
1049 | * @param minLen minimum length of return string |
1050 | * @return zero-padded s |
1051 | */ |
1052 | private String padIntString( int i, int minLen ) { |
1053 | String s = Integer.toString( i ); |
1054 | while ( s.length() < minLen ) { |
1055 | s = "0" + s; |
1056 | } |
1057 | return s; |
1058 | } |
1059 | |
1060 | /** |
1061 | * convert this duration to string representation |
1062 | * @return string representation of this duration |
1063 | */ |
1064 | public String toString() { |
1065 | StringBuffer sb = new StringBuffer(); |
1066 | sb.append( DURATION_DESIGNATOR ); |
1067 | if ( !alternative ) { |
1068 | // primary |
1069 | if ( isWeeks ) { |
1070 | sb.append( weeks ); |
1071 | if ( WEEK_DESIGNATOR == fractionUnit ) { |
1072 | appendFraction( sb ); |
1073 | } |
1074 | sb.append( WEEK_DESIGNATOR ); |
1075 | } |
1076 | else { |
1077 | if ( ( 0 == years ) && |
1078 | ( 0 == months ) && |
1079 | ( 0 == days ) && |
1080 | ( 0 == minutes ) && |
1081 | ( 0 == seconds ) ) { |
1082 | // zero |
1083 | sb.append( TIME_DESIGNATOR ); |
1084 | sb.append( '0' ); |
1085 | char unitDesignator = |
1086 | ( UNSET_FRACTION == fractionUnit ) |
1087 | ? SECOND_FRACTION |
1088 | : fractionUnit; |
1089 | sb.append( unitDesignator ); |
1090 | if ( UNSET_FRACTION != fractionUnit ) { |
1091 | appendFraction( sb ); |
1092 | } |
1093 | } |
1094 | else { |
1095 | // nonzero |
1096 | if ( ( 0 != years ) || ( YEAR_FRACTION == fractionUnit ) ) { |
1097 | sb.append( years ); |
1098 | if ( YEAR_FRACTION == fractionUnit ) { |
1099 | appendFraction( sb ); |
1100 | } |
1101 | sb.append( YEAR_DESIGNATOR ); |
1102 | } |
1103 | if ( ( 0 != months ) || ( MONTH_FRACTION == fractionUnit ) ) { |
1104 | sb.append( months ); |
1105 | if ( MONTH_FRACTION == fractionUnit ) { |
1106 | appendFraction( sb ); |
1107 | } |
1108 | sb.append( MONTH_DESIGNATOR ); |
1109 | } |
1110 | if ( ( 0 != days ) || ( DAY_FRACTION == fractionUnit ) ) { |
1111 | sb.append( days ); |
1112 | if ( DAY_FRACTION == fractionUnit ) { |
1113 | appendFraction( sb ); |
1114 | } |
1115 | sb.append( DAY_DESIGNATOR ); |
1116 | } |
1117 | if ( ( 0 != hours ) || |
1118 | ( 0 != minutes ) || |
1119 | ( 0 != seconds ) ) { |
1120 | sb.append( TIME_DESIGNATOR ); |
1121 | if ( ( 0 != hours || ( HOUR_FRACTION == fractionUnit ) ) ) { |
1122 | sb.append( hours ); |
1123 | if ( HOUR_FRACTION == fractionUnit ) { |
1124 | appendFraction( sb ); |
1125 | } |
1126 | sb.append( HOUR_DESIGNATOR ); |
1127 | } |
1128 | if ( ( 0 != minutes ) || ( MINUTE_FRACTION == fractionUnit ) ) { |
1129 | sb.append( minutes ); |
1130 | if ( MINUTE_FRACTION == fractionUnit ) { |
1131 | appendFraction( sb ); |
1132 | } |
1133 | sb.append( MINUTE_DESIGNATOR ); |
1134 | } |
1135 | if ( ( 0 != seconds ) || ( SECOND_FRACTION == fractionUnit ) ) { |
1136 | sb.append( seconds ); |
1137 | if ( SECOND_FRACTION == fractionUnit ) { |
1138 | appendFraction( sb ); |
1139 | } |
1140 | sb.append( SECOND_DESIGNATOR ); |
1141 | } |
1142 | } |
1143 | } |
1144 | } |
1145 | } |
1146 | else { |
1147 | // alternative |
1148 | sb.append( padIntString( years, 4 ) ); |
1149 | if ( extended ) { |
1150 | sb.append( HYPHEN ); |
1151 | } |
1152 | sb.append( padIntString( months, 2 ) ); |
1153 | if ( extended ) { |
1154 | sb.append( HYPHEN ); |
1155 | } |
1156 | sb.append( padIntString( days, 2 ) ); |
1157 | if ( extended ) { |
1158 | sb.append( TIME_DESIGNATOR ); |
1159 | } |
1160 | sb.append( padIntString( hours, 2 ) ); |
1161 | if ( extended ) { |
1162 | sb.append( COLON ); |
1163 | } |
1164 | sb.append( padIntString( minutes, 2 ) ); |
1165 | if ( extended ) { |
1166 | sb.append( COLON ); |
1167 | } |
1168 | sb.append( padIntString( seconds, 2 ) ); |
1169 | if ( SECOND_FRACTION == fractionUnit ) { |
1170 | appendFraction( sb ); |
1171 | } |
1172 | } |
1173 | return sb.toString(); |
1174 | } |
1175 | |
1176 | /** |
1177 | * append this duration's fraction (including decimal separator) to given string buffer |
1178 | * @param sb string buffer to which to append this duration's fraction |
1179 | */ |
1180 | private void appendFraction( StringBuffer sb ) { |
1181 | DecimalFormatSymbols dfs = |
1182 | ( ( null == locale ) |
1183 | ? new DecimalFormatSymbols() |
1184 | : new DecimalFormatSymbols( locale ) ); |
1185 | sb.append( dfs.getDecimalSeparator() ); |
1186 | sb.append( fraction ); |
1187 | } |
1188 | |
1189 | } |