001: package net.sf.saxon.value;
002:
003: import net.sf.saxon.expr.XPathContext;
004: import net.sf.saxon.functions.Component;
005: import net.sf.saxon.om.FastStringBuffer;
006: import net.sf.saxon.trans.DynamicError;
007: import net.sf.saxon.trans.XPathException;
008: import net.sf.saxon.type.*;
009:
010: import java.util.StringTokenizer;
011:
012: /**
013: * A value of type xs:duration
014: */
015:
016: public class DurationValue extends AtomicValue {
017:
018: protected boolean negative = false;
019: protected int years = 0;
020: protected int months = 0;
021: protected int days = 0;
022: protected int hours = 0;
023: protected int minutes = 0;
024: protected int seconds = 0;
025: protected int microseconds = 0;
026: private boolean normalized = false;
027:
028: /**
029: * Private constructor for internal use
030: */
031:
032: protected DurationValue() {
033: }
034:
035: public DurationValue(boolean positive, int years, int months,
036: int days, int hours, int minutes, int seconds,
037: int microseconds) {
038: this .negative = !positive;
039: this .years = years;
040: this .months = months;
041: this .days = days;
042: this .hours = hours;
043: this .minutes = minutes;
044: this .seconds = seconds;
045: this .microseconds = microseconds;
046: normalized = (months < 12 && hours < 24 && minutes < 60
047: && seconds < 60 && microseconds < 1000000);
048: }
049:
050: /**
051: * Constructor: create a duration value from a supplied string, in
052: * ISO 8601 format [+|-]PnYnMnDTnHnMnS
053: */
054:
055: public DurationValue(CharSequence s) throws XPathException {
056: StringTokenizer tok = new StringTokenizer(trimWhitespace(s)
057: .toString(), "-+.PYMDTHS", true);
058: try {
059: if (!tok.hasMoreElements())
060: badDuration("empty string", s);
061: String part = (String) tok.nextElement();
062: if ("+".equals(part)) {
063: part = (String) tok.nextElement();
064: } else if ("-".equals(part)) {
065: negative = true;
066: part = (String) tok.nextElement();
067: }
068: if (!"P".equals(part))
069: badDuration("missing 'P'", s);
070: int state = 0;
071: while (tok.hasMoreElements()) {
072: part = (String) tok.nextElement();
073: if ("T".equals(part)) {
074: state = 4;
075: part = (String) tok.nextElement();
076: }
077: int value = Integer.parseInt(part);
078: if (!tok.hasMoreElements())
079: badDuration("missing unit letter at end", s);
080: char delim = ((String) tok.nextElement()).charAt(0);
081: switch (delim) {
082: case 'Y':
083: if (state > 0)
084: badDuration("Y is out of sequence", s);
085: years = value;
086: state = 1;
087: break;
088: case 'M':
089: if (state == 4 || state == 5) {
090: minutes = value;
091: state = 6;
092: break;
093: } else if (state == 0 || state == 1) {
094: months = value;
095: state = 2;
096: break;
097: } else {
098: badDuration("M is out of sequence", s);
099: }
100: case 'D':
101: if (state > 2)
102: badDuration("D is out of sequence", s);
103: days = value;
104: state = 3;
105: break;
106: case 'H':
107: if (state != 4)
108: badDuration("H is out of sequence", s);
109: hours = value;
110: state = 5;
111: break;
112: case '.':
113: if (state < 4 || state > 6)
114: badDuration("misplaced decimal point", s);
115: seconds = value;
116: state = 7;
117: break;
118: case 'S':
119: if (state < 4 || state > 7)
120: badDuration("S is out of sequence", s);
121: if (state == 7) {
122: while (part.length() < 6)
123: part += "0";
124: if (part.length() > 6)
125: part = part.substring(0, 6);
126: microseconds = Integer.parseInt(part);
127: } else {
128: seconds = value;
129: }
130: state = 8;
131: break;
132: default:
133: badDuration("misplaced " + delim, s);
134: }
135: }
136: // Note, duration values (unlike the two xdt: subtypes) are not normalized
137:
138: } catch (NumberFormatException err) {
139: badDuration("non-numeric component", s);
140: }
141: }
142:
143: protected void badDuration(String msg, CharSequence s)
144: throws XPathException {
145: DynamicError err = new DynamicError("Invalid duration value '"
146: + s + "' (" + msg + ')');
147: err.setErrorCode("FORG0001");
148: throw err;
149: }
150:
151: /**
152: * Convert to target data type
153: * @param requiredType an integer identifying the required atomic type
154: * @param context
155: * @return an AtomicValue, a value of the required type; or an ErrorValue
156: */
157:
158: public AtomicValue convertPrimitive(BuiltInAtomicType requiredType,
159: boolean validate, XPathContext context) {
160: //System.err.println("Convert duration " + getClass() + " to " + Type.getTypeName(requiredType));
161: switch (requiredType.getPrimitiveType()) {
162: case Type.DURATION:
163: case Type.ANY_ATOMIC:
164: case Type.ITEM:
165: return this ;
166: case Type.STRING:
167: return new StringValue(getStringValueCS());
168: case Type.UNTYPED_ATOMIC:
169: return new UntypedAtomicValue(getStringValueCS());
170: case Type.YEAR_MONTH_DURATION:
171: // if (days!=0 || hours!=0 || minutes!=0 || seconds!=0 || microseconds!=0) {
172: // ValidationException err = new ValidationException(
173: // "Cannot convert to yearMonthDuration because some components are non-zero");
174: // //err.setXPathContext(context);
175: // err.setErrorCode("FORG0001");
176: // return new ValidationErrorValue(err);
177: // } else {
178: return MonthDurationValue.fromMonths((years * 12 + months)
179: * (negative ? -1 : +1));
180: // }
181: case Type.DAY_TIME_DURATION:
182: // if (years!=0 || months!=0) {
183: // ValidationException err = new ValidationException(
184: // "Cannot convert to dayTimeDuration because some components are non-zero");
185: // //err.setXPathContext(context);
186: // err.setErrorCode("FORG0001");
187: // return new ValidationErrorValue(err);
188: // } else {
189: return new SecondsDurationValue((negative ? -1 : +1), days,
190: hours, minutes, seconds, microseconds);
191: // }
192: default:
193: ValidationException err = new ValidationException(
194: "Cannot convert duration to "
195: + requiredType.getDisplayName());
196: err.setErrorCode("XPTY0004");
197: err.setIsTypeError(true);
198: return new ValidationErrorValue(err);
199: }
200: }
201:
202: /**
203: * Normalize the duration, so that months<12, hours<24, minutes<60, seconds<60.
204: * At present we do this when converting to a string. It's possible that it should be done immediately
205: * on constructing the duration (so that component extraction functions get the normalized value).
206: * We're awaiting clarification of the spec...
207: */
208:
209: public DurationValue normalizeDuration() {
210: int totalMonths = years * 12 + months;
211: int years = totalMonths / 12;
212: int months = totalMonths % 12;
213: long totalMicroSeconds = ((((((days * 24L + hours) * 60L) + minutes) * 60L) + seconds) * 1000000L)
214: + microseconds;
215: int microseconds = (int) (totalMicroSeconds % 1000000L);
216: int totalSeconds = (int) (totalMicroSeconds / 1000000L);
217: int seconds = totalSeconds % 60;
218: int totalMinutes = totalSeconds / 60;
219: int minutes = totalMinutes % 60;
220: int totalHours = totalMinutes / 60;
221: int hours = totalHours % 24;
222: int days = totalHours / 24;
223: return new DurationValue(!negative, years, months, days, hours,
224: minutes, seconds, microseconds);
225:
226: }
227:
228: /**
229: * Convert the value to a string, using the serialization rules.
230: * For atomic values this is the same as a cast; for sequence values
231: * it gives a space-separated list. This method is refined for AtomicValues
232: * so that it never throws an Exception.
233: */
234:
235: public String getStringValue() {
236: return getStringValueCS().toString();
237: }
238:
239: /**
240: * Convert to string
241: * @return ISO 8601 representation.
242: */
243:
244: public CharSequence getStringValueCS() {
245:
246: // Note, Schema does not define a canonical representation. We omit all zero components, unless
247: // the duration is zero-length, in which case we output PT0S.
248:
249: if (years == 0 && months == 0 && days == 0 && hours == 0
250: && minutes == 0 && seconds == 0 && microseconds == 0) {
251: return "PT0S";
252: }
253:
254: if (!normalized) {
255: return normalizeDuration().getStringValueCS();
256: }
257:
258: FastStringBuffer sb = new FastStringBuffer(32);
259: if (negative) {
260: sb.append('-');
261: }
262: sb.append("P");
263: if (years != 0) {
264: sb.append(years + "Y");
265: }
266: if (months != 0) {
267: sb.append(months + "M");
268: }
269: if (days != 0) {
270: sb.append(days + "D");
271: }
272: if (hours != 0 || minutes != 0 || seconds != 0
273: || microseconds != 0) {
274: sb.append("T");
275: }
276: if (hours != 0) {
277: sb.append(hours + "H");
278: }
279: if (minutes != 0) {
280: sb.append(minutes + "M");
281: }
282: if (seconds != 0 || microseconds != 0) {
283: if (seconds != 0 && microseconds == 0) {
284: sb.append(seconds + "S");
285: } else {
286: long ms = (seconds * 1000000) + microseconds;
287: String mss = ms + "";
288: if (seconds == 0) {
289: mss = "0000000" + mss;
290: mss = mss.substring(mss.length() - 7);
291: }
292: sb.append(mss.substring(0, mss.length() - 6));
293: sb.append('.');
294: int lastSigDigit = mss.length() - 1;
295: while (mss.charAt(lastSigDigit) == '0') {
296: lastSigDigit--;
297: }
298: sb.append(mss.substring(mss.length() - 6,
299: lastSigDigit + 1));
300: sb.append('S');
301: }
302: }
303:
304: return sb;
305:
306: }
307:
308: /**
309: * Get length of duration in seconds, assuming an average length of month. (Note, this defines a total
310: * ordering on durations which is different from the partial order defined in XML Schema; XPath 2.0
311: * currently avoids defining an ordering at all. But the ordering here is consistent with the ordering
312: * of the two duration subtypes in XPath 2.0.)
313: */
314:
315: public double getLengthInSeconds() {
316: double a = years;
317: a = a * 12 + months;
318: a = a * (365.242199 / 12.0) + days;
319: a = a * 24 + hours;
320: a = a * 60 + minutes;
321: a = a * 60 + seconds;
322: a = a + ((double) microseconds / 1000000);
323: return (negative ? -a : a);
324: }
325:
326: /**
327: * Determine the data type of the exprssion
328: * @return Type.DURATION,
329: * @param th
330: */
331:
332: public ItemType getItemType(TypeHierarchy th) {
333: return Type.DURATION_TYPE;
334: }
335:
336: /**
337: * Convert to Java object (for passing to external functions)
338: */
339:
340: public Object convertToJava(Class target, XPathContext context)
341: throws XPathException {
342: if (target.isAssignableFrom(DurationValue.class)) {
343: return this ;
344: } else if (target == String.class
345: || target == CharSequence.class) {
346: return getStringValue();
347: } else if (target == Object.class) {
348: return getStringValue();
349: } else {
350: Object o = super .convertToJava(target, context);
351: if (o == null) {
352: DynamicError err = new DynamicError(
353: "Conversion of duration to " + target.getName()
354: + " is not supported");
355: err.setXPathContext(context);
356: err.setErrorCode("SAXON:0000");
357: }
358: return o;
359: }
360: }
361:
362: /**
363: * Get a component of the value
364: */
365:
366: public AtomicValue getComponent(int component)
367: throws XPathException {
368: // We define the method at this level, but the function signatures for the component
369: // extraction functions ensure that the method is only ever called on the two subtypes
370: switch (component) {
371: case Component.YEAR:
372: return new IntegerValue((negative ? -years : years));
373: case Component.MONTH:
374: return new IntegerValue((negative ? -months : months));
375: case Component.DAY:
376: return new IntegerValue((negative ? -days : days));
377: case Component.HOURS:
378: return new IntegerValue((negative ? -hours : hours));
379: case Component.MINUTES:
380: return new IntegerValue((negative ? -minutes : minutes));
381: case Component.SECONDS:
382: FastStringBuffer sb = new FastStringBuffer(16);
383: String ms = ("000000" + microseconds);
384: ms = ms.substring(ms.length() - 6);
385: sb.append((negative ? "-" : "") + seconds + '.' + ms);
386: return DecimalValue.makeDecimalValue(sb, false);
387: default:
388: throw new IllegalArgumentException(
389: "Unknown component for duration: " + component);
390: }
391: }
392:
393: /**
394: * Compare the value to another duration value
395: * @param other The other dateTime value
396: * @return negative value if this one is the shorter duration, 0 if they are equal,
397: * positive value if this one is the longer duration. For this purpose, a year is considered
398: * to be equal to 365.242199 days.
399: * @throws ClassCastException if the other value is not a DurationValue (the parameter
400: * is declared as Object to satisfy the Comparable interface)
401: */
402:
403: // public int compareTo(Object other) {
404: // if (!(other instanceof DurationValue)) {
405: // throw new ClassCastException("Duration values are not comparable to " + other.getClass());
406: // }
407: // double s1 = this.getLengthInSeconds();
408: // double s2 = ((DurationValue)other).getLengthInSeconds();
409: // if (s1==s2) return 0;
410: // if (s1<s2) return -1;
411: // return +1;
412: // }
413: /**
414: * Test if the two durations are of equal length. Note: this function is defined
415: * in XPath 2.0, but its semantics are currently unclear.
416: */
417:
418: public boolean equals(Object other) {
419: if (!(other instanceof DurationValue)) {
420: throw new ClassCastException(
421: "Duration values are not comparable to "
422: + other.getClass());
423: }
424: DurationValue d1 = normalizeDuration();
425: DurationValue d2 = ((DurationValue) other).normalizeDuration();
426: return d1.negative == d2.negative && d1.years == d2.years
427: && d1.months == d2.months && d1.days == d2.days
428: && d1.hours == d2.hours && d1.minutes == d2.minutes
429: && d1.seconds == d2.seconds
430: && d1.microseconds == d2.microseconds;
431: }
432:
433: public int hashCode() {
434: return new Double(getLengthInSeconds()).hashCode();
435: }
436:
437: /**
438: * Compare two values for equality. This supports the matching rules in XML Schema, which say that two
439: * durations are equal only if all six components are equal (thus 12 months does not equal one year).
440: */
441:
442: public boolean schemaEquals(Value obj) {
443: if (obj instanceof AtomicValue) {
444: obj = ((AtomicValue) obj).getPrimitiveValue();
445: }
446: if (obj instanceof DurationValue) {
447: DurationValue d = (DurationValue) obj;
448: return years == d.years && months == d.months
449: && days == d.days && hours == d.hours
450: && minutes == d.minutes && seconds == d.seconds
451: && microseconds == d.microseconds;
452: } else {
453: return false;
454: }
455: }
456:
457: /**
458: * Add two durations
459: */
460:
461: public DurationValue add(DurationValue other, XPathContext context)
462: throws XPathException {
463: throw new DynamicError(
464: "Only subtypes of xs:duration can be added");
465: }
466:
467: /**
468: * Subtract two durations
469: */
470:
471: public DurationValue subtract(DurationValue other,
472: XPathContext context) throws XPathException {
473: throw new DynamicError(
474: "Only subtypes of xs:duration can be subtracted");
475: }
476:
477: /**
478: * Multiply a duration by a number
479: */
480:
481: public DurationValue multiply(double factor, XPathContext context)
482: throws XPathException {
483: throw new DynamicError(
484: "Only subtypes of xs:duration can be multiplied by a number");
485: }
486:
487: /**
488: * Divide a duration by a another duration
489: */
490:
491: public DecimalValue divide(DurationValue other, XPathContext context)
492: throws XPathException {
493: throw new DynamicError(
494: "Only subtypes of xs:duration can be divided by another duration");
495: }
496:
497: }
498:
499: //
500: // The contents of this file are subject to the Mozilla Public License Version 1.0 (the "License");
501: // you may not use this file except in compliance with the License. You may obtain a copy of the
502: // License at http://www.mozilla.org/MPL/
503: //
504: // Software distributed under the License is distributed on an "AS IS" basis,
505: // WITHOUT WARRANTY OF ANY KIND, either express or implied.
506: // See the License for the specific language governing rights and limitations under the License.
507: //
508: // The Original Code is: all this file.
509: //
510: // The Initial Developer of the Original Code is Michael H. Kay
511: //
512: // Portions created by (your name) are Copyright (C) (your legal entity). All Rights Reserved.
513: //
514: // Contributor(s): none.
515: //
|