/* * Copyright 2013 Guidewire Software, Inc. */ package gw.xml.date; import java.util.TimeZone; import java.util.Calendar; import java.text.SimpleDateFormat; import java.util.GregorianCalendar; import java.lang.Exception; import java.util.StringTokenizer; import java.lang.Integer; import java.lang.StringBuilder; import java.lang.IllegalStateException; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.ParseException; import java.text.DateFormat; public abstract class AbstractXmlDateType { private TimeZone _timeZone; private final boolean _includeYear; private final boolean _includeMonth; private final boolean _includeDay; private final boolean _includeTime; public AbstractXmlDateType( boolean y, boolean m, boolean d, boolean time ) { _includeYear = y; _includeMonth = m; _includeDay = d; _includeTime = time; } public TimeZone getTimeZone() { return _timeZone; } public void setTimeZone( TimeZone timeZone ) { _timeZone = timeZone; } protected int getYear() { return 0; } protected void setYear( int year ) { // ignore } protected int getMonth() { return 1; } protected void setMonth( int month ) { // ignore } protected int getDay() { return 1; } protected void setDay( int day ) { // ignore } protected int getHour() { return 0; } protected void setHour( int hour ) { // ignore } protected int getMinute() { return 0; } protected void setMinute( int minute ) { // ignore } protected BigDecimal getSecond() { return BigDecimal.ZERO; } protected void setSecond( BigDecimal second ) { // ignore } @Override public final String toString() { StringBuilder sb = new StringBuilder(); if ( _includeYear || _includeMonth || _includeDay ) { if ( _includeYear ) { sb.append( pad( String.valueOf( getYear() ), 4 ) ); } else { sb.append( "-" ); } if ( _includeMonth || _includeDay ) { sb.append( "-" ); if ( _includeMonth ) { sb.append( pad( String.valueOf( getMonth() ), 2 ) ); } if ( _includeDay ) { sb.append( "-" ); sb.append( pad( String.valueOf( getDay() ), 2 ) ); if ( _includeTime ) { sb.append( "T" ); } } } } if ( _includeTime ) { String s = String.valueOf( getSecond() ); int idx = s.indexOf( '.' ); if (idx < 0) { idx = s.length(); } while ( idx < 2 ) { s = "0" + s; idx++; } sb.append( pad( String.valueOf( getHour() ), 2 ) ); sb.append( ':' ); sb.append( pad( String.valueOf( getMinute() ), 2 ) ); sb.append( ':' ); sb.append( s ); } if ( getTimeZone() != null ) { if ( getTimeZone().equals( java.util.TimeZone.getTimeZone( "GMT" ) ) ) { sb.append( "Z" ); } else { DateFormat f = new SimpleDateFormat( "Z" ); f.setTimeZone( getTimeZone() ); Calendar cal = toCalendar(); String tz = f.format( cal.getTime() ); tz = tz.substring( 0, 3 ) + ':' + tz.substring( 3 ); sb.append(tz); } } return sb.toString(); } private String pad( String s, int count ) { while ( s.length() < count ) { s = "0" + s; } return s; } public final Calendar toCalendar() { if (getTimeZone() == null) { throw new IllegalStateException( "TimeZone must be set" ); } return toCalendarInternal( getTimeZone() ); } public final Calendar toCalendar( TimeZone tzDefault ) { return toCalendarInternal( getTimeZone() == null ? tzDefault : getTimeZone() ); } private Calendar toCalendarInternal( TimeZone tzDefault ) { if ( tzDefault == null ) { tzDefault = java.util.TimeZone.getTimeZone( "GMT" ); } Calendar cal = new GregorianCalendar(); cal.clear(); cal.setTimeZone( tzDefault ); if ( _includeYear ) { cal.set( Calendar.YEAR, getYear() ); } if ( _includeMonth ) { cal.set( Calendar.MONTH, getMonth() - 1 ); } if ( _includeDay ) { cal.set( Calendar.DAY_OF_MONTH, getDay() ); } if ( _includeTime ) { cal.set( Calendar.HOUR_OF_DAY, getHour() ); cal.set( Calendar.MINUTE, getMinute() ); cal.set( Calendar.SECOND, getSecond().intValue() ); BigDecimal[] tmp = getSecond().remainder( BigDecimal.ONE ).multiply( new BigDecimal( "1000" ) ).divideAndRemainder( BigDecimal.ONE ); if ( tmp[1].compareTo( new BigDecimal( "0.5" ) ) >= 0 ) { cal.set( Calendar.MILLISECOND, tmp[0].intValue() + 1 ); } else { cal.set( Calendar.MILLISECOND, tmp[0].intValue() ); } } return cal; } protected final void parseString( String s ) throws ParseException { RuntimeException ex = new RuntimeException(); try { StringTokenizer st = new StringTokenizer(s, "TZ+-:", true); if ( _includeYear || _includeMonth || _includeDay ) { if (_includeYear) { String token = st.nextToken(); int yearSign = 1; if ( token.equals( "-" ) ) { yearSign = -1; token = st.nextToken(); } setYear( Integer.parseInt(token) * yearSign ); } else { checkToken( st, "-", ex ); } if ( _includeMonth || _includeDay ) { checkToken(st, "-", ex); if (_includeMonth) { setMonth( Integer.parseInt( st.nextToken() ) ); if ( getMonth() > 12 || getMonth() <= 0 ) { throw ex; } } if (_includeDay) { checkToken(st, "-", ex); setDay( Integer.parseInt( st.nextToken() ) ); int maxDays = 31; if ( getMonth() == 9 || getMonth() == 4 || getMonth() == 6 || getMonth() == 11 ) { maxDays = 30; } else if ( getMonth() == 2 ) { maxDays = 28; if ( getYear() % 4 == 0 ) { maxDays = 29; if ( getYear() % 100 == 0 ) { maxDays = 28; if ( getYear() % 400 == 0 ) { maxDays = 29; } } } } if ( getDay() > maxDays || getDay() <= 0 ) { throw ex; } if ( _includeTime ) { checkToken(st, "T", ex); } } } } if (_includeTime) { setHour( Integer.parseInt( st.nextToken() ) ); if ( getHour() >= 24 ) { throw ex; } checkToken(st, ":", ex); setMinute( Integer.parseInt( st.nextToken() ) ); if ( getMinute() >= 60 ) { throw ex; } checkToken(st, ":", ex); String secondString = st.nextToken(); setSecond( new BigDecimal( secondString ) ); if ( getSecond().compareTo( new BigDecimal( "60" ) ) >= 0 ) { throw ex; } } if (st.hasMoreTokens()) { String token = st.nextToken(); if (token.equals( "Z" ) ) { token = "GMT"; } else { token = "GMT" + token; } StringBuilder tzString = new StringBuilder(token); while (st.hasMoreTokens()) { tzString.append(st.nextToken()); } String tzs = tzString.toString(); if ( ! tzs.matches( "GMT([+-]\\d\\d:\\d\\d)?" ) ) { throw ex; } setTimeZone( TimeZone.getTimeZone( tzs ) ); if ( ! getTimeZone().getID().equals( tzs ) ) { throw ex; } } if (st.hasMoreTokens()) { throw ex; } } catch ( Exception e ) { throw new ParseException("Could not parse date: " + s, -1); } } private static void checkToken( StringTokenizer st, String token, RuntimeException ex ) { String nextToken = st.nextToken(); if ( ! token.equals( nextToken ) ) { throw ex; } } protected final void getCalendarFields( Calendar cal, boolean useTimeZone ) { if ( _includeYear ) { setYear( cal.get( Calendar.YEAR ) ); } if ( _includeMonth ) { setMonth( cal.get( Calendar.MONTH ) + 1 ); } if ( _includeDay ) { setDay( cal.get( Calendar.DAY_OF_MONTH ) ); } if ( _includeTime ) { setHour( cal.get( Calendar.HOUR_OF_DAY ) ); setMinute( cal.get( Calendar.MINUTE ) ); setSecond( new BigDecimal( String.valueOf( cal.get( Calendar.SECOND ) ) ) ); setSecond( getSecond().add( new BigDecimal( String.valueOf( cal.get( Calendar.MILLISECOND ) ) ).divide( new BigDecimal( "1000" ) ) ) ); } if ( useTimeZone ) { setTimeZone( cal.getTimeZone() ); } else { setTimeZone( null ); } } // This algorithm is specified in the following specification: // http://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes public void add( XmlDuration d ) { BigDecimal sMonth = new BigDecimal( String.valueOf( getMonth() ) ); BigDecimal sDay = new BigDecimal( String.valueOf( getDay() ) ); BigDecimal sYear = new BigDecimal( String.valueOf( getYear() ) ); BigDecimal sHour = new BigDecimal( String.valueOf( getHour() ) ); BigDecimal sMinute = new BigDecimal( String.valueOf( getMinute() ) ); BigDecimal sSecond = getSecond(); // months (may be modified additionally below) BigDecimal temp = sMonth.add( new BigDecimal( d.isNegative() ? d.getMonths().negate() : d.getMonths() ) ); BigDecimal eMonth = modulo( temp, BigDecimal.ONE, new BigDecimal( "13" ) ); BigDecimal carry = fQuotient( temp, BigDecimal.ONE, new BigDecimal( "13" ) ); // Years (may be modified additionally below) BigDecimal eYear = sYear.add( new BigDecimal( d.isNegative() ? d.getYears().negate() : d.getYears() ) ).add( carry ); // Zone //var eZone = sZone // Seconds temp = sSecond.add( d.isNegative() ? d.getSeconds().negate() : d.getSeconds() ); BigDecimal eSecond = modulo( temp, new BigDecimal( "60" ) ); carry = fQuotient( temp, new BigDecimal( "60" ) ); // Minutes temp = sMinute.add( new BigDecimal( d.isNegative() ? d.getMinutes().negate() : d.getMinutes() ) ).add( carry ); BigDecimal eMinute = modulo( temp, new BigDecimal( "60" ) ); carry = fQuotient( temp, new BigDecimal( "60" ) ); // Hours temp = sHour.add( new BigDecimal( d.isNegative() ? d.getHours().negate() : d.getHours() ) ).add( carry ); BigDecimal eHour = modulo( temp, new BigDecimal( "24" ) ); carry = fQuotient( temp, new BigDecimal( "24" ) ); // Days BigDecimal tempDays; if ( sDay.compareTo( maximumDayInMonthFor( eYear, eMonth ) ) > 0 ) { tempDays = maximumDayInMonthFor( eYear, eMonth ); } else if ( sDay.compareTo( BigDecimal.ONE ) < 0 ) { tempDays = BigDecimal.ONE; } else { tempDays = sDay; } BigDecimal eDay = tempDays.add( new BigDecimal( d.isNegative() ? d.getDays().negate() : d.getDays() ) ).add( carry ); while ( true ) { if ( eDay.compareTo( BigDecimal.ONE ) < 0 ) { eDay = eDay.add( maximumDayInMonthFor( eYear, eMonth.subtract( BigDecimal.ONE ) ) ); carry = new BigDecimal( "-1" ); } else if ( eDay.compareTo( maximumDayInMonthFor( eYear, eMonth ) ) > 0 ) { eDay = eDay.subtract( maximumDayInMonthFor( eYear, eMonth ) ); carry = BigDecimal.ONE; } else { break; } temp = eMonth.add( carry ); eMonth = modulo( temp, BigDecimal.ONE, new BigDecimal( "13" ) ); eYear = eYear.add( fQuotient( temp, BigDecimal.ONE, new BigDecimal( "13" ) ) ); } setMonth( eMonth.intValue() ); setDay( eDay.intValue() ); setYear( eYear.intValue() ); setHour( eHour.intValue() ); setMinute( eMinute.intValue() ); setSecond( eSecond ); } private BigDecimal fQuotient( BigDecimal a, BigDecimal b ) { BigDecimal result = a.divide( b, 0, RoundingMode.DOWN ); if ( a.compareTo( BigDecimal.ZERO ) < 0 ) { result = result.subtract( BigDecimal.ONE ); } return result; } private BigDecimal modulo( BigDecimal a, BigDecimal b ) { BigDecimal result = a.remainder( b ); if ( result.compareTo( BigDecimal.ZERO ) < 0 ) { result = result.add( b ); } return result; } private BigDecimal fQuotient( BigDecimal a, BigDecimal low, BigDecimal high ) { if ( a.compareTo( BigDecimal.ZERO ) < 0 ) { return fQuotient( a.add( low ), high.subtract( low ) ); } else { return fQuotient( a.subtract( low ), high.subtract( low ) ); } } private BigDecimal modulo( BigDecimal a, BigDecimal low, BigDecimal high ) { return modulo( a.subtract( low ), high.subtract( low ) ).add( low ); } private BigDecimal maximumDayInMonthFor( BigDecimal yearValue, BigDecimal monthValue ) { BigDecimal m = modulo( monthValue, BigDecimal.ONE, new BigDecimal( "13" ) ); BigDecimal y = yearValue.add( fQuotient( monthValue, BigDecimal.ONE, new BigDecimal( 13 ) ) ); switch ( m.intValue() ) { case 9: // september case 4: // april case 6: // june case 11: // november return new BigDecimal( "30" ); case 2: return ( modulo( y, new BigDecimal( "400" ) ).intValue() == 0 || ( modulo( y, new BigDecimal( "100" ) ).intValue() != 0 ) && modulo( y, new BigDecimal( "4" ) ).intValue() == 0 ) ? new BigDecimal( "29" ) : new BigDecimal( "28" ); default: return new BigDecimal( "31" ); } } @Override public final boolean equals( Object o ) { if ( o.getClass() == this.getClass() ) { AbstractXmlDateType other = (AbstractXmlDateType) o; if ( ( getTimeZone() == null ) == ( other.getTimeZone() == null ) ) { // compare timestamps Calendar thisCal = toCalendarInternal( getTimeZone() ); Calendar otherCal = other.toCalendarInternal( other.getTimeZone() ); return thisCal.getTime().getTime() == otherCal.getTime().getTime(); } } return false; } @Override public final int hashCode() { int result = toCalendarInternal( getTimeZone() ).getTime().hashCode(); if ( getTimeZone() == null ) { result = -result; } return result; } }