[Mulgara-svn] r974 - trunk/src/jar/util/java/org/mulgara/util

pag at mulgara.org pag at mulgara.org
Fri Jun 6 04:03:07 UTC 2008


Author: pag
Date: 2008-06-05 21:03:07 -0700 (Thu, 05 Jun 2008)
New Revision: 974

Added:
   trunk/src/jar/util/java/org/mulgara/util/LexicalDateTime.java
   trunk/src/jar/util/java/org/mulgara/util/LexicalDateTimeUnitTest.java
   trunk/src/jar/util/java/org/mulgara/util/Timezone.java
Log:
Utilities for converting the lexical form of XSD dateTimes into bytes appropriate for storing, while not canonicalizing the data, and retaining full lexical information.

Added: trunk/src/jar/util/java/org/mulgara/util/LexicalDateTime.java
===================================================================
--- trunk/src/jar/util/java/org/mulgara/util/LexicalDateTime.java	                        (rev 0)
+++ trunk/src/jar/util/java/org/mulgara/util/LexicalDateTime.java	2008-06-06 04:03:07 UTC (rev 974)
@@ -0,0 +1,444 @@
+/*
+ * The contents of this file are subject to the Open Software License
+ * Version 3.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.opensource.org/licenses/osl-3.0.txt
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+ * the License for the specific language governing rights and limitations
+ * under the License.
+ */
+
+package org.mulgara.util;
+
+import java.text.ParseException;
+import java.nio.ByteBuffer;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+import static org.joda.time.DateTimeZone.UTC;
+
+/**
+ * This class represents a dateTime value, preserving its lexical representation exactly.
+ * It stores the value of the dateTime in the canonical form, but also contains values which
+ * allow the preservation of the non-canonical format.
+ *
+ * @created Jun 5, 2008
+ * @author Paul Gearon
+ * @copyright &copy; 2008 <a href="http://www.topazproject.org/">The Topaz Project</a>
+ * @licence <a href="{@docRoot}/../../LICENCE.txt">Open Software License v3.0</a>
+ */
+public class LexicalDateTime {
+
+  /** The character for separating date elements */
+  private static final char DATE_SEPARATOR = '-';
+
+  /** The character for separating the date part from the time part */
+  private static final char DATE_TIME_SEPARATOR = 'T';
+
+  /** The character for separating time elements */
+  private static final char TIME_SEPARATOR = ':';
+
+  /** The character for separating the milliseconds from the seconds */
+  private static final char MILLI_SEPARATOR = '.';
+
+  /** The string form for the character separating the milliseconds from the seconds */
+  private static final String MILLI_SEPARATOR_STR = ".";
+
+  /** The character for indicating the UTC timezone (Zulu time). */
+  private static final char ZULU = 'Z';
+
+  /** The string form for the character indicating the UTC timezone (Zulu time). */
+  private static final String ZULU_STR = "Z";
+
+  /** The character for indicating a positive timezone offset */
+  private static final char POS_TZ = '+';
+
+  /** The character for indicating a negative timezone offset */
+  private static final char NEG_TZ = '-';
+
+  /** The hour value for midnight */
+  private static final int MIDNIGHT = 24;
+
+  /** The string representation of midnight when the midnight flag is set */
+  private static final String MIDNIGHT_STR = "24:00:00";
+
+  /** Standard start of parsing error messages */
+  private static final String BAD_FORMAT = "Bad format in ";
+
+  /** Output format for the dateTime */
+  private static final String LEXICAL_PATTERN = "yyyy-MM-dd'T'HH:mm:ss";
+
+  /** Output format for the date portion of the dateTime */
+  private static final String SHORT_PATTERN = "yyyy-MM-dd'T'";
+
+  /** The formatter used for converting the dateTime into a lexical form */
+  private static final DateTimeFormatter MAIN_FORMATTER = DateTimeFormat.forPattern(LEXICAL_PATTERN);
+
+  /** A supplemantary formatter for outputting the date, when the time has to be represented in non-canonical form */
+  private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormat.forPattern(SHORT_PATTERN);
+
+  /** The number of milliseconds in a second */
+  private static final int MILLIS = 1000;
+
+  /** The number of milliseconds in a minute */
+  private static final int MILLIS_IN_MINUTE = MILLIS * 60;
+
+  /** The number of milliseconds in an hour */
+  private static final long MILLIS_IN_HOUR = MILLIS_IN_MINUTE * 60;
+
+  /** The bit used to encode the localFlag */
+  private static final byte LOCAL_BIT = 0x02;
+
+  /** The bit used to encode the midnightFlag */
+  private static final byte MIDNIGHT_BIT = 0x01;
+
+  /** The mask for the timezone bits */
+  private static final byte TZ_MASK = (byte)0xFC;
+
+  /** Number of bytes in a Long */
+  private static final int SIZEOF_LONG = Long.SIZE / 8;
+
+  /** The offset of the timezone data in an encoded buffer */
+  private static final int TZ_OFFSET = SIZEOF_LONG;
+
+  /** The offset of the fractional seconds decimal places in an encoded buffer */
+  private static final int PLACES_OFFSET = TZ_OFFSET + 1;
+
+  /** The milliseconds since the epoch */
+  private final long millis;
+
+  /** The hours offset for the time */
+  private final int tzHours;
+
+  /** The minutes offset for the timezone. A multiple of 15. */
+  private final int tzMinutes;
+
+  /** Indicates that the time was supplied as 24:00:00. */
+  private final boolean midnight;
+
+  /** The number of decimal places used to represent the milliseconds. No greater than 3. */
+  private final byte milliPlaces;
+
+  /** Indicates no supplied timezone. This defaults to the local timezone. */
+  private final boolean localFlag;
+
+  /** Indicates ZULU time, which is equivalent to +00:00. */
+  private final boolean zuluFlag;
+
+  /** A DateTime corresponding to this object. Only created if needed. */
+  private DateTime cachedDateTime = null;
+
+  /**
+   * This constructor is used to set each field explicitly, when all such information is available.
+   * No checking is performed on the consistency of the millisecond value, though some minimal testing is done on flags.
+   * Whether tested or not, the following should hold:
+   * <ul>
+   * <li>if <code>isMidnight</code> is set, then <code>millis</code> must be a multiple of 24 hours.</li>
+   * <li>if <code>isLocalTz</code> is set, then <code>tzHours</code> and <code>tzMinutes</code> should be 0,
+   *     and <code>isZulu</code> must be false.</li>
+   * <li>if <code>isZulu</code> is set, then <code>tzHours</code> and <code>tzMinutes</code> must be 0.</li>
+   * <li><code>millis</code> % 10^(6 - milliPlaces) == 0</li>
+   * </ul>
+   * @param millis The milliseconds since the epoch.
+   * @param tzHours The hour offset for the timezone.
+   * @param tzMinutes The minute offset for the timezone.
+   * @param isMidnight If the non-canonical form for midnight is used. "24:00:00"
+   * @param milliPlaces The number of decimal places used for representing millisecds as fractions of a second.
+   * @param isLocalTz Indicates no timezone information, so use the local default.
+   * @param isZulu Indicates that the timezone is "Zulu". This is equivalent to 00:00 and is represented as "Z".
+   * @throws IllegalArgumentException if the <code>isZulu</code> flag conflicts with the timezone values or the offsets.
+   */
+  public LexicalDateTime(long millis, int tzHours, int tzMinutes, boolean isMidnight, byte milliPlaces, boolean isLocalTz, boolean isZulu) {
+    this.millis = millis;
+    this.tzHours = tzHours;
+    this.tzMinutes = tzMinutes;
+    this.midnight = isMidnight;
+    this.milliPlaces = milliPlaces;
+    this.localFlag = isLocalTz;
+    this.zuluFlag = isZulu;
+    testTimezoneConsistency();
+  }
+
+  /**
+   * Internal constructor used with a dateTime object, and parsed fields.
+   * No checking is performed on the consistency of the millisecond value, though some minimal testing is done on flags.
+   * Whether tested or not, the following should hold:
+   * <ul>
+   * <li>if <code>isMidnight</code> is set, then <code>millis</code> plus the timezone offset must be a multiple
+   *     of 24 hours.</li>
+   * <li>if <code>isLocalTz</code> is set, then <code>tzHours</code> and <code>tzMinutes</code> should be 0,
+   *     and <code>isZulu</code> must be false.</li>
+   * <li>if <code>isZulu</code> is set, then <code>tzHours</code> and <code>tzMinutes</code> must be 0.</li>
+   * <li><code>millis</code> % 10^(3 - milliPlaces) == 0</li>
+   * <li>The <code>tzHours</code> and <code>tzMinutes</code> values should correspond to the values in the
+   *     <code>dateTime</code> field.</li>
+   * </ul>
+   * @param dateTime The dateTime object representing the time.
+   * @param tzHours The hour offset for the timezone.
+   * @param tzMinutes The minute offset for the timezone.
+   * @param isMidnight If the non-canonical form for midnight is used. "24:00:00"
+   * @param milliPlaces The number of decimal places used for representing millisecds as fractions of a second.
+   * @param isLocalTz Indicates no timezone information, so use the local default.
+   * @param isZulu Indicates that the timezone is "Zulu". This is equivalent to 00:00 and is represented as "Z".
+   * @throws IllegalArgumentException if the <code>isZulu</code> flag conflicts with the timezone values or the offsets.
+   */
+  private LexicalDateTime(DateTime dateTime, int tzHours, int tzMinutes, boolean isMidnight, byte milliPlaces, boolean isLocalTz, boolean isZulu) {
+    this.millis = dateTime.getMillis();
+    this.tzHours = tzHours;
+    this.tzMinutes = tzMinutes;
+    this.midnight = isMidnight;
+    this.cachedDateTime = dateTime;
+    this.milliPlaces = milliPlaces;
+    this.localFlag = isLocalTz;
+    this.zuluFlag = isZulu;
+    testTimezoneConsistency();
+  }
+
+  /**
+   * Convenience constructor which allows easy construction of a LexicalDateTime using the milliseconds since the epoch.
+   * @param millis Milliseconds since the epoch.
+   */
+  public LexicalDateTime(long millis) {
+    this.millis = millis;
+    long offset = DateTimeZone.getDefault().getOffset(0);
+    tzHours = (int)(offset / MILLIS_IN_HOUR);
+    tzMinutes = (int)(offset % MILLIS_IN_HOUR) / MILLIS_IN_MINUTE;
+    midnight = false;
+    cachedDateTime = null;
+    milliPlaces = 0;
+    localFlag = true;
+    zuluFlag = false;
+  }
+
+  /** Gets the number of milliseconds since the epoch. */
+  public long getMillis() {
+    return millis;
+  }
+
+  /** The the hour part of the offset for the timezone. */
+  public int getTZHour() {
+    return tzHours;
+  }
+
+  /** The the minute part of the offset for the timezone. */
+  public long getTZMinute() {
+    return tzMinutes;
+  }
+
+  /** Gets the flag that indicates that this time is a non-canonical form of midnight. */
+  public boolean isMidnight() {
+    return midnight;
+  }
+
+  /** Gets the flag that indicates no timezone is present, and the local default should be used. */
+  public boolean isLocal() {
+    return localFlag;
+  }
+
+  /** Gets the flag that indicates the Zulu timezone (UTC) and representation. */
+  public boolean isZulu() {
+    return zuluFlag;
+  }
+
+  /** Gets the number of decimal places to represent the fraction of a second. */
+  public byte getDecimalPlaces() {
+    return milliPlaces;
+  }
+
+  /**
+   * Fills in a ByteBuffer with the data required to encode this object.
+   * @param bb The {@link java.nio.ByteBuffer} to populate.
+   * @return The populated ByteBuffer.
+   */
+  public ByteBuffer encode(ByteBuffer bb) {
+    assert bb.limit() >= PLACES_OFFSET;
+    bb.putLong(0, millis);
+    bb.put(TZ_OFFSET, encodeTimezoneState());
+    bb.put(PLACES_OFFSET, milliPlaces);
+    return bb;
+  }
+
+  /**
+   * Creates a byte code for the timezone and flags of this dateTime.
+   * <table>
+   * <tr><td>bits 7-2</td><td>timezone code</td></tr>
+   * <tr><td >bit 1</td><td>local flag</td></tr>
+   * <tr><td>bit 0</td><td>midnight flag</td></tr>
+   * </table>
+   * @return a byte containing the timezone data.
+   */
+  public byte encodeTimezoneState() {
+    byte result = 0;
+    if (zuluFlag) result = Timezone.getZuluCode();
+    else result = new Timezone(tzHours, tzMinutes).getCode();
+    if (localFlag) result |= LOCAL_BIT;
+    if (midnight) result |= MIDNIGHT_BIT;
+    return result;
+  }
+
+  /**
+   * Decodes a {@link ByteBuffer} into a LexicalDateTime.
+   * @param bb The ByteBuffer to decode.
+   * @return a new LexicalDateTime structure.
+   */
+  public static LexicalDateTime decode(ByteBuffer bb) {
+    assert bb.limit() >= PLACES_OFFSET;
+    return decode(bb.getLong(0), bb.get(TZ_OFFSET), bb.get(PLACES_OFFSET));
+  }
+
+  /**
+   * Decodes a millisecond value and an encoded byte into a timezone and flags.
+   * @param millis The milliseconds since the epoch.
+   * @param timezoneState The encoded data representing the timezone.
+   * @param places The number of decimal places for the seconds representation.
+   * @return a new LexicalDateTime structure.
+   */
+  public static LexicalDateTime decode(long millis, byte timezoneState, byte places) {
+    boolean local = (timezoneState & LOCAL_BIT) != 0;
+    boolean midnight = (timezoneState & MIDNIGHT_BIT) != 0;
+    byte tzCode = (byte)(timezoneState & TZ_MASK);
+    boolean zulu = (tzCode == Timezone.getZuluCode());
+    Timezone tz = new Timezone(tzCode);
+    return new LexicalDateTime(millis, tz.getHour(), tz.getMinute(), midnight, places, local, zulu);
+  }
+
+  /** Return a lexical representation of this dateTime. */
+  public String toString() {
+    if (cachedDateTime == null) {
+      DateTimeZone dtz;
+      dtz = (localFlag) ? null : DateTimeZone.forOffsetHoursMinutes(tzHours, tzMinutes);
+      cachedDateTime = new DateTime(millis, dtz);
+    }
+    StringBuilder result;
+    if (!midnight) {
+      result = new StringBuilder(MAIN_FORMATTER.print(cachedDateTime));
+      if (milliPlaces > 0) {
+        result.append(MILLI_SEPARATOR_STR);
+        int place = MILLIS;
+        long fraction = millis;
+        if (fraction < 0) fraction = fraction % place + place;
+        for (int m = 0; m < milliPlaces; m++) {
+          fraction = fraction % place;
+          place /= 10;
+          result.append(fraction / place);
+        }
+      }
+    } else {
+      result = new StringBuilder(DATE_FORMATTER.print(cachedDateTime.plusDays(-1)));
+      result.append(MIDNIGHT_STR);
+      if (milliPlaces > 0) {
+        result.append(MILLI_SEPARATOR_STR);
+        for (int i = 0; i < milliPlaces; i++) result.append("0");
+      }
+    }
+    if (!localFlag) {
+      if (zuluFlag) result.append(ZULU_STR);
+      else result.append(String.format("%+03d:%02d", tzHours, tzMinutes));
+    }
+    return result.toString();
+  }
+
+  /**
+   * Parse a dateTime string. It <strong>must</strong> be of the form:
+   * ('-')? yyyy '-' MM '-' dd 'T' hh ':' mm ':' ss ( '.' s+ )? ( ( ('+'|'-')? hh ':' mm ) | 'Z' )?
+   * @param dt The dateTime string to parse.
+   * @return a new LexcalDateTime value.
+   * @throws ParseException If a character that doesn't match the above pattern is discovered.
+   */
+  public static LexicalDateTime parseDateTime(String dt) throws ParseException {
+    int pos = 0;
+    boolean negative = dt.charAt(pos) == '-';
+    if (negative) pos++;
+    int year = d(dt, pos++) * 1000 + d(dt, pos++) * 100 + d(dt, pos++) * 10 + d(dt, pos++);
+    while (dt.charAt(pos) != DATE_SEPARATOR) year = year * 10 + d(dt, pos++);
+    if (negative) year = -year;
+    if (dt.charAt(pos++) != DATE_SEPARATOR) throw new ParseException(BAD_FORMAT + "date: " + dt, pos - 1);
+    int month = d(dt, pos++) * 10 + d(dt, pos++);
+    if (dt.charAt(pos++) != DATE_SEPARATOR) throw new ParseException(BAD_FORMAT + "date: " + dt, pos - 1);
+    int day = d(dt, pos++) * 10 + d(dt, pos++);
+
+    if (dt.charAt(pos++) != DATE_TIME_SEPARATOR) throw new ParseException(BAD_FORMAT + "date/time: " + dt, pos - 1);
+
+    int hour = d(dt, pos++) * 10 + d(dt, pos++);
+    if (dt.charAt(pos++) != TIME_SEPARATOR) throw new ParseException(BAD_FORMAT + "time: " + dt, pos - 1);
+    int minute = d(dt, pos++) * 10 + d(dt, pos++);
+    if (dt.charAt(pos++) != TIME_SEPARATOR) throw new ParseException(BAD_FORMAT + "time: " + dt, pos - 1);
+    int second = d(dt, pos++) * 10 + d(dt, pos++);
+
+    int millisecs = 0;
+    byte milliPlaces = 0;
+    int lastPos = dt.length() - 1;
+    if (pos < lastPos) {
+      if (dt.charAt(pos) == MILLI_SEPARATOR) {
+        int place = MILLIS / 10;
+        int digit;
+        while (isDecimal((digit = dt.charAt(++pos) - '0'))) {
+          millisecs += digit * place;
+          if (milliPlaces++ > 3) throw new ParseException(BAD_FORMAT + "milliseconds: " + dt, pos);
+          place /= 10;
+          if (pos == lastPos) break;
+        }
+      }
+    }
+
+    boolean midnightFlag = false;
+    if (hour == MIDNIGHT) {
+      midnightFlag = true;
+      hour = 0;
+    }
+    if (midnightFlag && (minute > 0 || second > 0 || millisecs > 0)) throw new ParseException(BAD_FORMAT + "time: " + dt, pos);
+
+    boolean local = false;
+    int tzHour = 0;
+    int tzMinute = 0;
+    boolean zuluFlag = false;
+    DateTimeZone timezone = null;
+    if (pos <= lastPos) {
+      char tz = dt.charAt(pos++);
+      if (tz == ZULU) {
+        if (pos != lastPos + 1) throw new ParseException(BAD_FORMAT + "timezone: " + dt, pos);
+        timezone = UTC;
+        zuluFlag = true;
+      } else {
+        if (pos != lastPos - 4 || (tz != NEG_TZ && tz != POS_TZ)) throw new ParseException(BAD_FORMAT + "timezone: " + dt, pos);
+        tzHour = d(dt, pos++) * 10 + d(dt, pos++);
+        if (dt.charAt(pos++) != TIME_SEPARATOR) throw new ParseException(BAD_FORMAT + "timezone: " + dt, pos - 1);
+        tzMinute = d(dt, pos++) * 10 + d(dt, pos++);
+        if (tz == NEG_TZ) tzHour = -tzHour;
+        timezone = DateTimeZone.forOffsetHoursMinutes(tzHour, tzMinute);
+      }
+    } else {
+      local = true;
+    }
+
+    DateTime dateTime = new DateTime(year, month, day, hour, minute, second, millisecs, timezone);
+    if (midnightFlag) dateTime = dateTime.plusDays(1);
+    return new LexicalDateTime(dateTime, tzHour, tzMinute, midnightFlag, milliPlaces, local, zuluFlag);
+  }
+
+  /**
+   * Check that the timezone flags are consistent with one another.
+   * @throws IllegalArgumentException if the is an inconsistency in the timezone values.
+   */
+  private void testTimezoneConsistency() {
+    if (zuluFlag) {
+      if (localFlag) throw new IllegalArgumentException("Cannot have Zulu time and a \"default\" timezone");
+      if (tzHours != 0 || tzMinutes != 0) throw new IllegalArgumentException("Cannot have Zulu time and a timezone offset");
+    }
+    assert (millis % (int)Math.pow(10, 3 - milliPlaces)) == 0;
+  }
+
+  private static int d(String str, int i) throws ParseException {
+    int d = str.charAt(i) - '0';
+    if (d >= 10 || d < 0) throw new ParseException("Unexpected character: " + Character.toString(str.charAt(i)) + ". Expected numeric digit.", i);
+    return d;
+  }
+
+  private static boolean isDecimal(int i) {
+    return i < 10 && i >= 0;
+  }
+
+}

Added: trunk/src/jar/util/java/org/mulgara/util/LexicalDateTimeUnitTest.java
===================================================================
--- trunk/src/jar/util/java/org/mulgara/util/LexicalDateTimeUnitTest.java	                        (rev 0)
+++ trunk/src/jar/util/java/org/mulgara/util/LexicalDateTimeUnitTest.java	2008-06-06 04:03:07 UTC (rev 974)
@@ -0,0 +1,162 @@
+/*
+ * The contents of this file are subject to the Open Software License
+ * Version 3.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.opensource.org/licenses/osl-3.0.txt
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+ * the License for the specific language governing rights and limitations
+ * under the License.
+ */
+
+package org.mulgara.util;
+
+import java.nio.ByteBuffer;
+import java.text.ParseException;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+/**
+ * Test cases for LexicalDateTime
+ *
+ * @created Jun 5, 2008
+ * @author Paul Gearon
+ * @copyright &copy; 2008 <a href="http://www.topazproject.org/">The Topaz Project</a>
+ * @licence <a href="{@docRoot}/../../LICENCE.txt">Open Software License v3.0</a>
+ */
+public class LexicalDateTimeUnitTest extends TestCase {
+
+  String[] dtStrings = new String[] {
+      "2002-10-10T12:00:12.34-05:00",
+      "2002-10-10T12:00:12.34+05:00",
+      "2002-10-10T12:00:12.34+00:00",
+      "2002-10-10T00:00:12.34+00:00",
+      "2002-10-10T24:00:00.00+00:00",
+      "2002-10-10T12:00:12.34+10:00",
+      "2002-10-10T12:00:12.34Z",
+      "2002-10-10T00:00:12.34Z",
+      "2002-10-10T24:00:00.00Z",
+      "2002-10-10T12:00:12.345-05:00",
+      "2002-10-10T12:00:12.345+05:00",
+      "2002-10-10T12:00:12.345+00:00",
+      "2002-10-10T00:00:12.345+00:00",
+      "2002-10-10T24:00:00.000+00:00",
+      "2002-10-10T12:00:12.345+10:00",
+      "2002-10-10T12:00:12.345Z",
+      "2002-10-10T00:00:12.345Z",
+      "2002-10-10T24:00:00.000Z"
+  };
+  
+  String[] negStrings = new String[] {
+      "-0002-10-10T12:00:12.34-05:00",
+      "-0002-10-10T12:00:12.34+05:00",
+      "-0002-10-10T12:00:12.34+00:00",
+      "-0002-10-10T00:00:12.34+00:00",
+      "-0002-10-10T24:00:00.00+00:00",
+      "-0002-10-10T12:00:12.34+10:00",
+      "-0002-10-10T12:00:12.34Z",
+      "-0002-10-10T00:00:12.34Z",
+      "-0002-10-10T24:00:00.00Z",
+      "-0002-10-10T12:00:12.345-05:00",
+      "-0002-10-10T12:00:12.345+05:00",
+      "-0002-10-10T12:00:12.345+00:00",
+      "-0002-10-10T00:00:12.345+00:00",
+      "-0002-10-10T24:00:00.000+00:00",
+      "-0002-10-10T12:00:12.345+10:00",
+      "-0002-10-10T12:00:12.345Z",
+      "-0002-10-10T00:00:12.345Z",
+      "-0002-10-10T24:00:00.000Z"
+  };
+
+  String[] largeStrings = new String[] {
+      "12002-10-10T12:00:12.34-05:00",
+      "120002-10-10T12:00:12.34-05:00",
+      "-12002-10-10T12:00:12.34-05:00",
+      "-120002-10-10T12:00:12.34-05:00"
+  };
+
+  public LexicalDateTimeUnitTest(String name) {
+    super(name);
+  }
+
+  /**
+   * Hook for test runner to obtain a test suite from.
+   * @return The test suite to run.
+   */
+  public static Test suite() {
+    TestSuite suite = new TestSuite();
+    suite.addTest(new LexicalDateTimeUnitTest("testParseDateTime"));
+    suite.addTest(new LexicalDateTimeUnitTest("testEncode"));
+    suite.addTest(new LexicalDateTimeUnitTest("testEncodeTimezoneState"));
+    return suite;
+  }
+
+  /**
+   * Default test runner.
+   *
+   * @param args The command line arguments
+   */
+  public static void main(String[] args) {
+    junit.textui.TestRunner.run(suite());
+  }
+
+  /**
+   * Test method for {@link org.mulgara.util.LexicalDateTime#parseDateTime(java.lang.String)}.
+   */
+  public void testParseDateTime() throws ParseException {
+    parseDateTimeHelper(dtStrings);
+    parseDateTimeHelper(negStrings);
+    parseDateTimeHelper(largeStrings);
+  }
+
+  void parseDateTimeHelper(String[] strings) throws ParseException {
+    for (String l: strings) {
+      LexicalDateTime dt = LexicalDateTime.parseDateTime(l);
+      assertEquals(l, dt.toString());
+    }
+  }
+
+  /**
+   * Test method for {@link org.mulgara.util.LexicalDateTime#encode(java.nio.ByteBuffer)}.
+   */
+  public void testEncode() throws ParseException {
+    encodeHelper(dtStrings);
+    encodeHelper(negStrings);
+    encodeHelper(largeStrings);
+  }
+
+  void encodeHelper(String[] strings) throws ParseException {
+    for (String l: strings) {
+      LexicalDateTime dt = LexicalDateTime.parseDateTime(l);
+
+      ByteBuffer bb = ByteBuffer.allocate(16);
+      LexicalDateTime newDt = LexicalDateTime.decode(dt.encode(bb));
+
+      assertEquals(l, newDt.toString());
+    }
+  }
+
+  /**
+   * Test method for {@link org.mulgara.util.LexicalDateTime#encodeTimezoneState()}.
+   */
+  public void testEncodeTimezoneState() throws ParseException {
+    encodeTimezoneStateHelper(dtStrings);
+    encodeTimezoneStateHelper(negStrings);
+    encodeTimezoneStateHelper(largeStrings);
+  }
+
+  void encodeTimezoneStateHelper(String[] strings) throws ParseException {
+    for (String l: strings) {
+      LexicalDateTime dt = LexicalDateTime.parseDateTime(l);
+      long millis = dt.getMillis();
+      byte tzstate = dt.encodeTimezoneState();
+      byte dec = dt.getDecimalPlaces();
+
+      LexicalDateTime newDt = LexicalDateTime.decode(millis, tzstate, dec);
+      assertEquals(l, newDt.toString());
+    }
+  }
+}

Added: trunk/src/jar/util/java/org/mulgara/util/Timezone.java
===================================================================
--- trunk/src/jar/util/java/org/mulgara/util/Timezone.java	                        (rev 0)
+++ trunk/src/jar/util/java/org/mulgara/util/Timezone.java	2008-06-06 04:03:07 UTC (rev 974)
@@ -0,0 +1,145 @@
+/*
+ * The contents of this file are subject to the Open Software License
+ * Version 3.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.opensource.org/licenses/osl-3.0.txt
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+ * the License for the specific language governing rights and limitations
+ * under the License.
+ */
+
+package org.mulgara.util;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Maps from a known hour:minute offset for a timezone into a 6 bit code, and back.
+ *
+ * @created Jun 5, 2008
+ * @author Paul Gearon
+ * @copyright &copy; 2008 <a href="http://www.topazproject.org/">The Topaz Project</a>
+ * @licence <a href="{@docRoot}/../../LICENCE.txt">Open Software License v3.0</a>
+ */
+public class Timezone {
+
+  /** A mask for restricting bit patterns to a byte (removing sign extension) */
+  private static final int BYTE_MASK = 0xFF;
+
+  /** The construct that holds the offset details */
+  private HourMinute hm;
+
+  /** The internal code for the timezone offset. */
+  private byte internalCode;
+
+  /**
+   * Constructs a Timezone using a code.
+   * @param code The code for a known timezone.
+   * @throws IllegalArgumentException The code does not correspond to a timezone in the database.
+   */
+  public Timezone(byte code) {
+    if ((code & 3) != 0) throw new IllegalArgumentException("Invalid bit pattern in the timezone code");
+    internalCode = (byte)((code & BYTE_MASK) >> 2);
+    if (internalCode > timezoneList.length || internalCode < 0) throw new IllegalArgumentException("Unknown timezone code: " + code);
+    hm = (code == ZULU_CODE) ? ZULU : timezoneList[internalCode];
+  }
+
+  /**
+   * Constructs a Timezone using an hour:minute offset.
+   * @param hour The hour offset of the timezone. This cannot encode ZULU time.
+   * @param minute The minute offset of the timezone.
+   * @throws IllegalArgumentException The timezone is not in the database of known timezones.
+   */
+  public Timezone(int hour, int minute) {
+    hm = new HourMinute(hour, minute);
+    Byte c = tzCodes.get(hm);
+    if (c == null) throw new IllegalArgumentException("Timezone is not in official database: " + hm);
+    internalCode = c;
+  }
+
+  /** Gets the hour for this timezone. */
+  public int getHour() {
+    return hm.hour;
+  }
+
+  /** Gets the minute for this timezone. */
+  public int getMinute() {
+    return hm.minute;
+  }
+
+  /** Gets the code for this timezone. This is guaranteed to use the top 5 bits, and set the bottom 2 to 0.*/
+  public byte getCode() {
+    return (byte)(internalCode << 2);
+  }
+
+  /** Gets the code for the ZULU timezone. */
+  public static byte getZuluCode() {
+    return ZULU_CODE;
+  }
+
+  /** The ZULU timezone */
+  private static final HourMinute ZULU = new HourMinute(0, 0);
+
+  /** The database of known timezones. @see http://en.wikipedia.org/wiki/List_of_zoneinfo_timezones */
+  private static final HourMinute[] timezoneList = new HourMinute[] {
+    new HourMinute(-12, 0), new HourMinute(-11, 0), new HourMinute(-10, 0), new HourMinute(-9, 30),
+    new HourMinute(-9, 0), new HourMinute(-8, 0), new HourMinute(-7, 0), new HourMinute(-6, 0),
+    new HourMinute(-5, 0), new HourMinute(-4, 30), new HourMinute(-4, 0), new HourMinute(-3, 30),
+    new HourMinute(-3, 0), new HourMinute(-2, 0), new HourMinute(-1, 0), ZULU,
+    new HourMinute(1, 0), new HourMinute(2, 0), new HourMinute(3, 0), new HourMinute(3, 30),
+    new HourMinute(4, 0), new HourMinute(4, 30), new HourMinute(5, 0), new HourMinute(5, 30),
+    new HourMinute(5, 45), new HourMinute(6, 0), new HourMinute(6, 30), new HourMinute(7, 0),
+    new HourMinute(8, 0), new HourMinute(8, 45), new HourMinute(9, 0), new HourMinute(9, 30),
+    new HourMinute(10, 0), new HourMinute(10, 30), new HourMinute(11, 0), new HourMinute(11, 30),
+    new HourMinute(12, 0), new HourMinute(12, 45), new HourMinute(13, 0), new HourMinute(14, 0)
+  };
+
+  /** A map of timezones to their code. */
+  private static final Map<HourMinute,Byte> tzCodes;
+
+  /** A special code for ZULU, to distinguish it from 00:00. This is an external code, so the lowest 2 bits must be 0. */
+  public static final byte ZULU_CODE = (byte)(timezoneList.length << 2);
+
+  // populates the tzCodes with the timezoneList
+  static {
+    Map<HourMinute,Byte> writeMap = new HashMap<HourMinute,Byte>();
+    for (byte t = 0; t < timezoneList.length; t++) writeMap.put(timezoneList[t], t);
+    tzCodes = Collections.unmodifiableMap(writeMap);
+  }
+
+  /**
+   * A private structure for associating an hour and minute together.
+   */
+  private static class HourMinute {
+    /** The hour value. */
+    public final int hour;
+    /** The minute value. */
+    public final int minute;
+
+    /** Constructs an hour and minute tuple */
+    public HourMinute(int h, int m) {
+      hour = h;
+      minute = m;
+    }
+
+    /** @inheritDoc */
+    public int hashCode() {
+      return (hour * 4) + (minute / 15);
+    }
+
+    /** @inheritDoc */
+    public boolean equals(Object o) {
+      return (o instanceof HourMinute) && ((HourMinute)o).hour == hour && ((HourMinute)o).minute == minute;
+    }
+
+    /** @inheritDoc */
+    public String toString() {
+      if (hour == 0 && minute == 0) return "00:00";
+      return String.format("%02d:%02d", hour, minute);
+    }
+  }
+
+}




More information about the Mulgara-svn mailing list