/** * Duration item ({@code xs:duration}). * * @author BaseX Team 2005-16, BSD License * @author Christian Gruen */ public class Dur extends ADateDur { /** Pattern for one or more digits. */ static final String DP = "(\\d+)"; /** Date pattern. */ private static final Pattern DUR = Pattern.compile( "(-?)P(" + DP + "Y)?(" + DP + "M)?(" + DP + "D)?(T(" + DP + "H)?(" + DP + "M)?((\\d+|\\d*\\.\\d+)?S)?)?"); /** Number of months. */ long mon; /** * Constructor. * * @param value value * @param ii input info * @throws QueryException query exception */ public Dur(final byte[] value, final InputInfo ii) throws QueryException { this(value, AtomType.DUR, ii); } /** * Constructor. * * @param type item type */ Dur(final Type type) { super(type); } /** * Constructor. * * @param dur duration */ public Dur(final Dur dur) { this(dur, AtomType.DUR); } /** * Constructor. * * @param dur duration * @param type item type */ private Dur(final Dur dur, final Type type) { this(type); mon = dur.mon; sec = dur.sec == null ? BigDecimal.ZERO : dur.sec; } /** * Constructor. * * @param value value * @param type item type * @param ii input info * @throws QueryException query exception */ private Dur(final byte[] value, final Type type, final InputInfo ii) throws QueryException { this(type); final String val = Token.string(value).trim(); final Matcher mt = DUR.matcher(val); if (!mt.matches() || val.endsWith("P") || val.endsWith("T")) throw dateError(value, XDURR, ii); yearMonth(value, mt, ii); dayTime(value, mt, 6, ii); } /** * Initializes the yearMonth component. * * @param vl value * @param mt matcher * @param ii input info * @throws QueryException query exception */ void yearMonth(final byte[] vl, final Matcher mt, final InputInfo ii) throws QueryException { final long y = mt.group(2) != null ? toLong(mt.group(3), true, ii) : 0; final long m = mt.group(4) != null ? toLong(mt.group(5), true, ii) : 0; mon = y * 12 + m; double v = y * 12d + m; if (!mt.group(1).isEmpty()) { mon = -mon; v = -v; } if (v <= Long.MIN_VALUE || v >= Long.MAX_VALUE) throw DURRANGE_X_X.get(ii, type, vl); } /** * Initializes the dayTime component. * * @param vl value * @param mt matcher * @param p first matching position * @param ii input info * @throws QueryException query exception */ void dayTime(final byte[] vl, final Matcher mt, final int p, final InputInfo ii) throws QueryException { final long d = mt.group(p) != null ? toLong(mt.group(p + 1), true, ii) : 0; final long h = mt.group(p + 3) != null ? toLong(mt.group(p + 4), true, ii) : 0; final long m = mt.group(p + 5) != null ? toLong(mt.group(p + 6), true, ii) : 0; final BigDecimal s = mt.group(p + 7) != null ? toDecimal(mt.group(p + 8), true, ii) : BigDecimal.ZERO; sec = s.add(BigDecimal.valueOf(d).multiply(DAYSECONDS)) .add(BigDecimal.valueOf(h).multiply(BD3600)) .add(BigDecimal.valueOf(m).multiply(BD60)); if (!mt.group(1).isEmpty()) sec = sec.negate(); final double v = sec.doubleValue(); if (v <= Long.MIN_VALUE || v >= Long.MAX_VALUE) throw DURRANGE_X_X.get(ii, type, vl); } @Override public final long yea() { return mon / 12; } @Override public final long mon() { return mon % 12; } @Override public final long day() { return sec.divideToIntegralValue(DAYSECONDS).longValue(); } @Override public final long hou() { return tim() / 3600; } @Override public final long min() { return tim() % 3600 / 60; } @Override public final BigDecimal sec() { return sec.remainder(BD60); } /** * Returns the time. * * @return time */ private long tim() { return sec.remainder(DAYSECONDS).longValue(); } @Override public byte[] string(final InputInfo ii) { final TokenBuilder tb = new TokenBuilder(); final int ss = sec.signum(); if (mon < 0 || ss < 0) tb.add('-'); date(tb); time(tb); if (mon == 0 && ss == 0) tb.add("T0S"); return tb.finish(); } /** * Adds the date to the specified token builder. * * @param tb token builder */ final void date(final TokenBuilder tb) { tb.add('P'); final long y = yea(); if (y != 0) { tb.addLong(Math.abs(y)); tb.add('Y'); } final long m = mon(); if (m != 0) { tb.addLong(Math.abs(m)); tb.add('M'); } final long d = day(); if (d != 0) { tb.addLong(Math.abs(d)); tb.add('D'); } } /** * Adds the time to the specified token builder. * * @param tb token builder */ final void time(final TokenBuilder tb) { if (sec.remainder(DAYSECONDS).signum() == 0) return; tb.add('T'); final long h = hou(); if (h != 0) { tb.addLong(Math.abs(h)); tb.add('H'); } final long m = min(); if (m != 0) { tb.addLong(Math.abs(m)); tb.add('M'); } final BigDecimal sc = sec(); if (sc.signum() == 0) return; tb.add(Token.chopNumber(Token.token(sc.abs().toPlainString()))).add('S'); } @Override public final boolean eq( final Item it, final Collation coll, final StaticContext sc, final InputInfo ii) throws QueryException { final Dur d = (Dur) (it instanceof Dur ? it : type.cast(it, null, null, ii)); final BigDecimal s1 = sec == null ? BigDecimal.ZERO : sec; final BigDecimal s2 = d.sec == null ? BigDecimal.ZERO : d.sec; return mon == d.mon && s1.compareTo(s2) == 0; } @Override public int diff(final Item it, final Collation coll, final InputInfo ii) throws QueryException { throw diffError(ii, it, this); } @Override public final Duration toJava() { return ADate.DF.newDuration(Token.string(string(null))); } @Override public final int hash(final InputInfo ii) { return (int) (31 * mon + (sec == null ? 0 : sec.doubleValue())); } @Override public final String toString() { return Util.info("\"%\"", string(null)); } }
/** * Abstract super class for date items. * * @author BaseX Team 2005-15, BSD License * @author Christian Gruen */ public abstract class ADate extends ADateDur { /** Maximum value for computations on year value based on long range. */ static final long MAX_YEAR = (long) (Long.MAX_VALUE / 365.2425) - 2; /** Minimum year value. */ static final long MIN_YEAR = -MAX_YEAR; /** Constant for counting negative years (divisible by 400). */ private static final long ADD_NEG = (MAX_YEAR / 400 + 1) * 400; /** Pattern for two digits. */ static final String DD = "(\\d{2})"; /** Year pattern. */ static final String YEAR = "(-?(000[1-9]|00[1-9]\\d|0[1-9]\\d{2}|[1-9]\\d{3,}))"; /** Date pattern. */ static final String ZONE = "((\\+|-)" + DD + ':' + DD + "|Z)?"; /** Day per months. */ static final byte[] DAYS = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; /** Date pattern. */ private static final Pattern DATE = Pattern.compile(YEAR + '-' + DD + '-' + DD + ZONE); /** Time pattern. */ private static final Pattern TIME = Pattern.compile(DD + ':' + DD + ':' + "(\\d{2}(\\.\\d+)?)" + ZONE); /** * Year. * * <ul> * <li>1 - {@code Long#MAX_VALUE}-1: AD * <li>0 - {@link Long#MIN_VALUE}: BC, +1 added * <li>{@link Long#MAX_VALUE}: undefined * </ul> */ long yea = Long.MAX_VALUE; /** Month ({@code 0-11}). {@code -1}: undefined. */ byte mon = -1; /** Day ({@code 0-30}). {@code -1}: undefined. */ byte day = -1; /** Hour ({@code 0-59}). {@code -1}: undefined. */ byte hou = -1; /** Minute ({@code 0-59}). {@code -1}: undefined. */ byte min = -1; /** Timezone in minutes ({@code -14*60-14*60}). {@link Short#MAX_VALUE}: undefined. */ short tz = Short.MAX_VALUE; /** Data factory. */ static final DatatypeFactory DF; static { try { DF = DatatypeFactory.newInstance(); } catch (final Exception ex) { throw Util.notExpected(ex); } } /** * Constructor. * * @param type item type * @param date date reference */ ADate(final Type type, final ADate date) { super(type); yea = date.yea; mon = date.mon; day = date.day; hou = date.hou; min = date.min; sec = date.sec; tz = date.tz; } /** * Constructor. * * @param type item type */ ADate(final Type type) { super(type); } /** * Initializes the date format. * * @param d input * @param e example format * @param ii input info * @throws QueryException query exception */ final void date(final byte[] d, final String e, final InputInfo ii) throws QueryException { final Matcher mt = DATE.matcher(Token.string(d).trim()); if (!mt.matches()) throw dateError(d, e, ii); yea = toLong(mt.group(1), false, ii); // +1 is added to BC values to simplify computations if (yea < 0) yea++; mon = (byte) (Strings.toInt(mt.group(3)) - 1); day = (byte) (Strings.toInt(mt.group(4)) - 1); if (mon < 0 || mon >= 12 || day < 0 || day >= dpm(yea, mon)) throw dateError(d, e, ii); if (yea <= MIN_YEAR || yea > MAX_YEAR) throw DATERANGE_X_X.get(ii, type, chop(d, ii)); zone(mt, 5, d, ii); } /** * Initializes the time format. * * @param d input format * @param e expected format * @param ii input info * @throws QueryException query exception */ final void time(final byte[] d, final String e, final InputInfo ii) throws QueryException { final Matcher mt = TIME.matcher(Token.string(d).trim()); if (!mt.matches()) throw dateError(d, e, ii); hou = (byte) Strings.toInt(mt.group(1)); min = (byte) Strings.toInt(mt.group(2)); sec = toDecimal(mt.group(3), false, ii); if (min >= 60 || sec.compareTo(BD60) >= 0 || hou > 24 || hou == 24 && (min > 0 || sec.compareTo(BigDecimal.ZERO) > 0)) throw dateError(d, e, ii); zone(mt, 5, d, ii); if (hou == 24) { hou = 0; add(DAYSECONDS); } } /** * Initializes the timezone. * * @param matcher matcher * @param pos first matching position * @param value value * @param ii input info * @throws QueryException query exception */ final void zone(final Matcher matcher, final int pos, final byte[] value, final InputInfo ii) throws QueryException { final String z = matcher.group(pos); if (z == null) return; if ("Z".equals(z)) { tz = 0; } else { final int th = Strings.toInt(matcher.group(pos + 2)); final int tm = Strings.toInt(matcher.group(pos + 3)); if (th > 14 || tm > 59 || th == 14 && tm != 0) throw INVALIDZONE_X.get(ii, value); final int mn = th * 60 + tm; tz = (short) ("-".equals(matcher.group(pos + 1)) ? -mn : mn); } } /** * Adds/subtracts the specified dayTime duration. * * @param dur duration * @param plus plus/minus flag */ final void calc(final DTDur dur, final boolean plus) { add(plus ? dur.sec : dur.sec.negate()); } /** * Adds/subtracts the specified yearMonth duration. * * @param dur duration * @param plus plus/minus flag * @param ii input info * @throws QueryException query exception */ final void calc(final YMDur dur, final boolean plus, final InputInfo ii) throws QueryException { final long m = plus ? dur.mon : -dur.mon; final long mn = mon + m; mon = (byte) mod(mn, 12); yea += div(mn, 12); day = (byte) Math.min(dpm(yea, mon) - 1, day); if (yea <= MIN_YEAR || yea > MAX_YEAR) throw YEARRANGE_X.get(ii, yea); } /** * Adds the specified dayTime duration. * * @param add value to be added */ private void add(final BigDecimal add) { // normalized modulo: sc % 60 vs. (-sc + sc % 60 + 60 + sc) % 60 final BigDecimal sc = sec().add(add); sec = sc.signum() >= 0 ? sc.remainder(BD60) : sc.negate().add(sc.remainder(BD60)).add(BD60).add(sc).remainder(BD60); final long mn = Math.max(min(), 0) + div(sc.longValue(), 60); min = (byte) mod(mn, 60); final long ho = Math.max(hou, 0) + div(mn, 60); hou = (byte) mod(ho, 24); final long da = div(ho, 24); final long[] ymd = ymd(days().add(BigDecimal.valueOf(da))); yea = ymd[0]; mon = (byte) ymd[1]; day = (byte) ymd[2]; } /** * Returns a normalized module value for negative and positive values. * * @param value input value * @param mod modulo * @return result */ private static long mod(final long value, final int mod) { return value > 0 ? value % mod : (Long.MAX_VALUE / mod * mod + value) % mod; } /** * Returns a normalized division value for negative and positive values. * * @param value input value * @param div divisor * @return result */ private static long div(final long value, final int div) { return value < 0 ? (value + 1) / div - 1 : value / div; } /** * Adjusts the timezone. * * @param zone timezone * @param spec indicates if zone has been specified (may be {@code null}) * @param ii input info * @throws QueryException query exception */ public abstract void timeZone(final DTDur zone, final boolean spec, final InputInfo ii) throws QueryException; /** * Adjusts the timezone. * * @param zone timezone * @param spec indicates if zone has been specified (may be {@code null}) * @param ii input info * @throws QueryException query exception */ void tz(final DTDur zone, final boolean spec, final InputInfo ii) throws QueryException { final short t; if (spec && zone == null) { t = Short.MAX_VALUE; } else { if (zone == null) { final Calendar c = Calendar.getInstance(); t = (short) ((c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET)) / 60000); } else { t = (short) (zone.min() + zone.hou() * 60); if (zone.sec().signum() != 0) throw ZONESEC_X.get(ii, zone); if (Math.abs(t) > 60 * 14 || zone.day() != 0) throw INVALZONE_X.get(ii, zone); } // change time if two competing time zones exist if (tz != Short.MAX_VALUE) add(BigDecimal.valueOf(60L * (t - tz))); } tz = t; } @Override public final long yea() { return yea > 0 ? yea : yea - 1; } @Override public final long mon() { return mon + 1; } @Override public final long day() { return day + 1; } @Override public final long hou() { return hou; } @Override public final long min() { return min; } @Override public final BigDecimal sec() { return sec == null ? BigDecimal.ZERO : sec; } /** * Returns the timezone in minutes. * * @return time zone */ public final int tz() { return tz; } /** * Returns if the timezone is defined. * * @return time zone */ public final boolean tzDefined() { return tz != Short.MAX_VALUE; } @Override public byte[] string(final InputInfo ii) { final TokenBuilder tb = new TokenBuilder(); final boolean ymd = yea != Long.MAX_VALUE; if (ymd) { if (yea <= 0) tb.add('-'); prefix(tb, Math.abs(yea()), 4); tb.add('-'); prefix(tb, mon(), 2); tb.add('-'); prefix(tb, day(), 2); } if (hou >= 0) { if (ymd) tb.add('T'); prefix(tb, hou(), 2); tb.add(':'); prefix(tb, min(), 2); tb.add(':'); if (sec.intValue() < 10) tb.add('0'); tb.addExt(Token.chopNumber(Token.token(sec().abs().toPlainString()))); } zone(tb); return tb.finish(); } /** * Adds the time zone to the specified token builder. * * @param tb token builder */ void zone(final TokenBuilder tb) { if (tz == Short.MAX_VALUE) return; if (tz == 0) { tb.add('Z'); } else { tb.add(tz > 0 ? '+' : '-'); prefix(tb, Math.abs(tz) / 60, 2); tb.add(':'); prefix(tb, Math.abs(tz) % 60, 2); } } /** * Prefixes the specified number of zero digits before a number. * * @param tb token builder * @param number number to be printed * @param zero maximum number of zero digits */ static void prefix(final TokenBuilder tb, final long number, final int zero) { final byte[] t = Token.token(number); for (int i = t.length; i < zero; i++) tb.add('0'); tb.add(t); } @Override public final boolean eq( final Item it, final Collation coll, final StaticContext sc, final InputInfo ii) throws QueryException { final ADate d = (ADate) (it instanceof ADate ? it : type.cast(it, null, null, ii)); final BigDecimal d1 = seconds().add(days().multiply(DAYSECONDS)); final BigDecimal d2 = d.seconds().add(d.days().multiply(DAYSECONDS)); return d1.compareTo(d2) == 0; } @Override public int hash(final InputInfo ii) throws QueryException { return seconds().add(days().multiply(DAYSECONDS)).intValue(); } @Override public int diff(final Item it, final Collation coll, final InputInfo ii) throws QueryException { final ADate d = (ADate) (it instanceof ADate ? it : type.cast(it, null, null, ii)); final BigDecimal d1 = seconds().add(days().multiply(DAYSECONDS)); final BigDecimal d2 = d.seconds().add(d.days().multiply(DAYSECONDS)); return d1.compareTo(d2); } @Override public final XMLGregorianCalendar toJava() { return DF.newXMLGregorianCalendar( yea == Long.MAX_VALUE ? null : BigInteger.valueOf(yea > 0 ? yea : yea - 1), mon >= 0 ? mon + 1 : Integer.MIN_VALUE, day >= 0 ? day + 1 : Integer.MIN_VALUE, hou >= 0 ? hou : Integer.MIN_VALUE, min >= 0 ? min : Integer.MIN_VALUE, sec != null ? sec.intValue() : Integer.MIN_VALUE, sec != null ? sec.remainder(BigDecimal.ONE) : null, tz == Short.MAX_VALUE ? Integer.MIN_VALUE : tz); } /** * Returns the date in seconds. * * @return seconds */ final BigDecimal seconds() { int z = tz; if (z == Short.MAX_VALUE) { // [CG] XQuery, DateTime: may be removed final long n = System.currentTimeMillis(); z = Calendar.getInstance().getTimeZone().getOffset(n) / 60000; } return (sec == null ? BigDecimal.ZERO : sec) .add(BigDecimal.valueOf(Math.max(0, hou) * 3600 + Math.max(0, min) * 60 - z * 60)); } /** * Returns a day count. * * @return days */ final BigDecimal days() { final long y = yea == Long.MAX_VALUE ? 1 : yea; return days(y + ADD_NEG, Math.max(mon, 0), Math.max(day, 0)); } /** * Returns a day count for the specified years, months and days. All values must be specified in * their internal representation (undefined values are supported, too). Algorithm is derived from * J R Stockton (http://www.merlyn.demon.co.uk/daycount.htm). * * @param year year * @param month month * @param day days * @return days */ private static BigDecimal days(final long year, final int month, final int day) { final long y = year - (month < 2 ? 1 : 0); final int m = month + (month < 2 ? 13 : 1); final int d = day + 1; return BD365 .multiply(BigDecimal.valueOf(y)) .add(BigDecimal.valueOf(y / 4 - y / 100 + y / 400 - 92 + d + (153 * m - 2) / 5)); } /** * Converts a day count into year, month and day components. Algorithm is derived from J R * Stockton (http://www.merlyn.demon.co.uk/daycount.htm). * * @param days day count * @return result array */ private static long[] ymd(final BigDecimal days) { BigDecimal d = days; BigDecimal t = d.add(BD36525).multiply(BD4).divideToIntegralValue(BD146097).subtract(BigDecimal.ONE); BigDecimal y = BD100.multiply(t); d = d.subtract(BD36524.multiply(t).add(t.divideToIntegralValue(BD4))); t = d.add(BD366).multiply(BD4).divideToIntegralValue(BD1461).subtract(BigDecimal.ONE); y = y.add(t); d = d.subtract(BD365.multiply(t).add(t.divideToIntegralValue(BD4))); final BigDecimal m = BD5.multiply(d).add(BD2).divideToIntegralValue(BD153); d = d.subtract(BD153.multiply(m).add(BD2).divideToIntegralValue(BD5)); long mm = m.longValue(); if (mm > 9) { mm -= 12; y = y.add(BigDecimal.ONE); } return new long[] {y.subtract(BigDecimal.valueOf(ADD_NEG)).longValue(), mm + 2, d.longValue()}; } /** * Returns days per month, considering leap years. * * @param yea year * @param mon month * @return days */ public static int dpm(final long yea, final int mon) { final byte l = DAYS[mon]; return mon == 1 && yea % 4 == 0 && (yea % 100 != 0 || yea % 400 == 0) ? l + 1 : l; } @Override public final String toString() { return Util.info("\"%\"", string(null)); } }