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 }