001 package com.thaiopensource.datatype.xsd;
002
003 import org.relaxng.datatype.ValidationContext;
004
005 import java.math.BigInteger;
006 import java.math.BigDecimal;
007 import java.util.Calendar;
008 import java.util.GregorianCalendar;
009
010 class DurationDatatype extends RegexDatatype implements OrderRelation {
011 static private final String PATTERN =
012 "-?P([0-9]+Y)?([0-9]+M)?([0-9]+D)?(T([0-9]+H)?([0-9]+M)?(([0-9]+(\\.[0-9]*)?|\\.[0-9]+)S)?)?";
013
014 DurationDatatype() {
015 super(PATTERN);
016 }
017
018 public boolean lexicallyAllows(String str) {
019 if (!super.lexicallyAllows(str))
020 return false;
021 char last = str.charAt(str.length()-1);
022 // This enforces that there must be at least one component
023 // and that T is omitted if all time components are omitted
024 return last != 'P' && last != 'T';
025 }
026
027 static private class Duration {
028 private final BigInteger years;
029 private final BigInteger months;
030 private final BigInteger days;
031 private final BigInteger hours;
032 private final BigInteger minutes;
033 private final BigDecimal seconds;
034
035 Duration(boolean negative,
036 BigInteger years, BigInteger months, BigInteger days,
037 BigInteger hours, BigInteger minutes, BigDecimal seconds) {
038 if (negative) {
039 this.years = years.negate();
040 this.months = months.negate();
041 this.days = days.negate();
042 this.hours = hours.negate();
043 this.minutes = minutes.negate();
044 this.seconds = seconds.negate();
045 }
046 else {
047 this.years = years;
048 this.months = months;
049 this.days = days;
050 this.hours = hours;
051 this.minutes = minutes;
052 this.seconds = seconds;
053 }
054 }
055
056 BigInteger getYears() {
057 return years;
058 }
059
060 BigInteger getMonths() {
061 return months;
062 }
063
064 BigInteger getDays() {
065 return days;
066 }
067
068 BigInteger getHours() {
069 return hours;
070 }
071
072 BigInteger getMinutes() {
073 return minutes;
074 }
075
076 BigDecimal getSeconds() {
077 return seconds;
078 }
079
080 public boolean equals(Object obj) {
081 if (!(obj instanceof Duration))
082 return false;
083 Duration other = (Duration)obj;
084 return (this.years.equals(other.years)
085 && this.months.equals(other.months)
086 && this.days.equals(other.days)
087 && this.hours.equals(other.hours)
088 && this.minutes.equals(other.minutes)
089 && this.seconds.compareTo(other.seconds) == 0);
090 }
091
092 public int hashCode() {
093 return (years.hashCode()
094 ^ months.hashCode()
095 ^ days.hashCode()
096 ^ hours.hashCode()
097 ^ minutes.hashCode()
098 ^ seconds.hashCode());
099 }
100 }
101
102 Object getValue(String str, ValidationContext vc) {
103 int t = str.indexOf('T');
104 if (t < 0)
105 t = str.length();
106 String date = str.substring(0, t);
107 String time = str.substring(t);
108 return new Duration(str.charAt(0) == '-',
109 getIntegerField(date, 'Y'),
110 getIntegerField(date, 'M'),
111 getIntegerField(date, 'D'),
112 getIntegerField(time, 'H'),
113 getIntegerField(time, 'M'),
114 getDecimalField(time, 'S'));
115
116 }
117
118 static private BigInteger getIntegerField(String str, char code) {
119 int end = str.indexOf(code);
120 if (end < 0)
121 return BigInteger.valueOf(0);
122 int start = end;
123 while (Character.isDigit(str.charAt(start - 1)))
124 --start;
125 return new BigInteger(str.substring(start, end));
126 }
127
128 static private BigDecimal getDecimalField(String str, char code) {
129 int end = str.indexOf(code);
130 if (end < 0)
131 return BigDecimal.valueOf(0);
132 int start = end;
133 while (!Character.isLetter(str.charAt(start - 1)))
134 --start;
135 return new BigDecimal(str.substring(start, end));
136 }
137
138 OrderRelation getOrderRelation() {
139 return this;
140 }
141
142 private static final int[] REF_YEAR_MONTHS = { 1696, 9, 1697, 2, 1903, 3, 1903, 7 };
143
144 public boolean isLessThan(Object obj1, Object obj2) {
145 Duration d1 = (Duration)obj1;
146 Duration d2 = (Duration)obj2;
147 BigInteger months1 = computeMonths(d1);
148 BigInteger months2 = computeMonths(d2);
149 BigDecimal seconds1 = computeSeconds(d1);
150 BigDecimal seconds2 = computeSeconds(d2);
151 switch (months1.compareTo(months2)) {
152 case -1:
153 if (seconds1.compareTo(seconds2) <= 0)
154 return true;
155 break;
156 case 0:
157 return seconds1.compareTo(seconds2) < 0;
158 case 1:
159 if (seconds1.compareTo(seconds2) >= 0)
160 return false;
161 break;
162 }
163 for (int i = 0; i < REF_YEAR_MONTHS.length; i += 2) {
164 BigDecimal total1 = daysPlusSeconds(computeDays(months1, REF_YEAR_MONTHS[i], REF_YEAR_MONTHS[i + 1]), seconds1);
165 BigDecimal total2 = daysPlusSeconds(computeDays(months2, REF_YEAR_MONTHS[i], REF_YEAR_MONTHS[i + 1]), seconds2);
166 if (total1.compareTo(total2) >= 0)
167 return false;
168 }
169 return true;
170 }
171
172 /**
173 * Returns the number of days spanned by a period of months starting with a particular
174 * reference year and month.
175 */
176 private static BigInteger computeDays(BigInteger months, int refYear, int refMonth) {
177 switch (months.signum()) {
178 case 0:
179 return BigInteger.valueOf(0);
180 case -1:
181 return computeDays(months.negate(), refYear, refMonth).negate();
182 }
183 // Complete cycle of Gregorian calendar is 400 years
184 BigInteger[] tem = months.divideAndRemainder(BigInteger.valueOf(400*12));
185 --refMonth; // use 0 base to match Java
186 int total = 0;
187 for (int rem = tem[1].intValue(); rem > 0; rem--) {
188 total += daysInMonth(refYear, refMonth);
189 if (++refMonth == 12) {
190 refMonth = 0;
191 refYear++;
192 }
193 }
194 // In the Gregorian calendar, there are 97 (= 100 + 4 - 1) leap years every 400 years.
195 return tem[0].multiply(BigInteger.valueOf(365*400 + 97)).add(BigInteger.valueOf(total));
196 }
197
198 private static int daysInMonth(int year, int month) {
199 switch (month) {
200 case Calendar.SEPTEMBER:
201 case Calendar.APRIL:
202 case Calendar.JUNE:
203 case Calendar.NOVEMBER:
204 return 30;
205 case Calendar.FEBRUARY:
206 return isLeapYear(year) ? 29 : 28;
207 }
208 return 31;
209 }
210
211 private static boolean isLeapYear(int year) {
212 return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
213 }
214
215 /**
216 * Returns the total number of seconds from a specified number of days and seconds.
217 */
218 private static BigDecimal daysPlusSeconds(BigInteger days, BigDecimal seconds) {
219 return seconds.add(new BigDecimal(days.multiply(BigInteger.valueOf(24*60*60))));
220 }
221
222 /**
223 * Returns the total number of months specified by the year and month fields of the duration
224 */
225 private static BigInteger computeMonths(Duration d) {
226 return d.getYears().multiply(BigInteger.valueOf(12)).add(d.getMonths());
227 }
228
229 /**
230 * Returns the total number of seconds specified by the days, hours, minuts and seconds fields of
231 * the duration.
232 */
233 private static BigDecimal computeSeconds(Duration d) {
234 return d.getSeconds().add(new BigDecimal(d.getDays().multiply(BigInteger.valueOf(24))
235 .add(d.getHours()).multiply(BigInteger.valueOf(60))
236 .add(d.getMinutes()).multiply(BigInteger.valueOf(60))));
237 }
238
239 public static void main(String[] args) {
240 DurationDatatype dt = new DurationDatatype();
241 System.err.println(dt.isLessThan(dt.getValue(args[0], null), dt.getValue(args[1], null)));
242 }
243 }