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    }