content: /**
   * Copyright 2010 The Apache Software Foundation
   *
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package com.alibaba.wasp.util;
  
  import com.alibaba.wasp.SQLErrorCode;
  import com.alibaba.wasp.jdbc.JdbcException;
  import com.alibaba.wasp.jdbc.value.Value;
  import com.alibaba.wasp.jdbc.value.ValueDate;
  import com.alibaba.wasp.jdbc.value.ValueNull;
  import com.alibaba.wasp.jdbc.value.ValueTime;
  import com.alibaba.wasp.jdbc.value.ValueTimestamp;
  
  import java.sql.Date;
  import java.sql.Time;
  import java.sql.Timestamp;
  import java.text.SimpleDateFormat;
  import java.util.Calendar;
  import java.util.GregorianCalendar;
  import java.util.Locale;
  import java.util.TimeZone;
  
  
  /**
   * This utility class contains time conversion functions.
   * <p>
   * Date value: a bit field with bits for the year, month, and day. Absolute day:
   * the day number (0 means 1970-01-01).
   */
  public class DateTimeUtils {
  
    /**
     * The number of milliseconds per day.
     */
    public static final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000L;
  
    private static final long NANOS_PER_DAY = MILLIS_PER_DAY * 1000000;
  
    private static final int SHIFT_YEAR = 9;
    private static final int SHIFT_MONTH = 5;
  
    private static final int[] NORMAL_DAYS_PER_MONTH = { 0, 31, 28, 31, 30, 31,
        30, 31, 31, 30, 31, 30, 31 };
  
    /**
     * Offsets of month within a year, starting with March, April,...
     */
    private static final int[] DAYS_OFFSET = { 0, 31, 61, 92, 122, 153, 184, 214,
        245, 275, 306, 337, 366 };
  
    private static int zoneOffset;
    private static Calendar cachedCalendar;
  
    private DateTimeUtils() {
      // utility class
    }
  
    static {
      getCalendar();
    }
  
    /**
     * Reset the calendar, for example after changing the default timezone.
     */
    public static void resetCalendar() {
      cachedCalendar = null;
      getCalendar();
    }
  
    private static Calendar getCalendar() {
      if (cachedCalendar == null) {
        cachedCalendar = Calendar.getInstance();
        zoneOffset = cachedCalendar.get(Calendar.ZONE_OFFSET);
      }
      return cachedCalendar;
    }
  
    /**
     * Convert the date to the specified time zone.
     * 
     * @param value
     *          the date (might be ValueNull)
     * @param calendar
     *          the calendar
     * @return the date using the correct time zone
     */
    public static Date convertDate(Value value, Calendar calendar) {
      if (value == ValueNull.INSTANCE) {
        return null;
      }
      ValueDate d = (ValueDate) value.convertTo(Value.DATE);
      Calendar cal = (Calendar) calendar.clone();
      cal.clear();
      cal.setLenient(true);
      long dateValue = d.getDateValue();
      setCalendarFields(cal, yearFromDateValue(dateValue),
          monthFromDateValue(dateValue), dayFromDateValue(dateValue), 0, 0, 0, 0);
      long ms = cal.getTimeInMillis();
      return new Date(ms);
    }
  
    /**
     * Convert the time to the specified time zone.
     * 
     * @param value
     *          the time (might be ValueNull)
     * @param calendar
     *          the calendar
     * @return the time using the correct time zone
     */
    public static Time convertTime(Value value, Calendar calendar) {
      if (value == ValueNull.INSTANCE) {
        return null;
      }
      ValueTime t = (ValueTime) value.convertTo(Value.TIME);
      Calendar cal = (Calendar) calendar.clone();
      cal.clear();
      cal.setLenient(true);
      long nanos = t.getNanos();
      long millis = nanos / 1000000;
      nanos -= millis * 1000000;
      long s = millis / 1000;
      millis -= s * 1000;
      long m = s / 60;
      s -= m * 60;
      long h = m / 60;
      m -= h * 60;
      setCalendarFields(cal, 1970, 1, 1, (int) h, (int) m, (int) s, (int) millis);
      long ms = cal.getTimeInMillis();
      return new Time(ms);
    }
  
    /**
     * Convert the timestamp to the specified time zone.
     * 
     * @param value
     *          the timestamp (might be ValueNull)
     * @param calendar
     *          the calendar
     * @return the timestamp using the correct time zone
     */
    public static Timestamp convertTimestamp(Value value, Calendar calendar) {
      if (value == ValueNull.INSTANCE) {
        return null;
      }
      ValueTimestamp ts = (ValueTimestamp) value.convertTo(Value.TIMESTAMP);
      Calendar cal = (Calendar) calendar.clone();
      cal.clear();
      cal.setLenient(true);
      long dateValue = ts.getDateValue();
      long nanos = ts.getNanos();
      long millis = nanos / 1000000;
      nanos -= millis * 1000000;
      long s = millis / 1000;
      millis -= s * 1000;
      long m = s / 60;
      s -= m * 60;
      long h = m / 60;
      m -= h * 60;
      setCalendarFields(cal, yearFromDateValue(dateValue),
          monthFromDateValue(dateValue), dayFromDateValue(dateValue), (int) h,
          (int) m, (int) s, (int) millis);
      long ms = cal.getTimeInMillis();
      Timestamp x = new Timestamp(ms);
      x.setNanos((int) (nanos + millis * 1000000));
      return x;
    }
  
    /**
     * Convert the date using the specified calendar.
     * 
     * @param x
     *          the date
     * @param calendar
     *          the calendar
     * @return the date
     */
    public static ValueDate convertDate(Date x, Calendar calendar) {
      if (calendar == null) {
        throw JdbcException.getInvalidValueException("calendar", null);
      }
      Calendar cal = (Calendar) calendar.clone();
      cal.setTimeInMillis(x.getTime());
      long dateValue = dateValueFromCalendar(cal);
      return ValueDate.fromDateValue(dateValue);
    }
  
    /**
     * Convert the time using the specified calendar.
     * 
     * @param x
     *          the time
     * @param calendar
     *          the calendar
     * @return the time
     */
    public static ValueTime convertTime(Time x, Calendar calendar) {
      if (calendar == null) {
        throw JdbcException.getInvalidValueException("calendar", null);
      }
      Calendar cal = (Calendar) calendar.clone();
      cal.setTimeInMillis(x.getTime());
      long nanos = nanosFromCalendar(cal);
      return ValueTime.fromNanos(nanos);
    }
  
    /**
     * Convert a date to the specified time zone.
     * 
     * @param x
     *          the date to convert
     * @param target
     *          the calendar with the target timezone
     * @return the milliseconds the milliseconds in UTC
     */
    public static long convertToLocal(java.util.Date x, Calendar target) {
      if (target == null) {
        throw JdbcException.getInvalidValueException("calendar", null);
      }
      target = (Calendar) target.clone();
      Calendar local = Calendar.getInstance();
      synchronized (local) {
        local.setTime(x);
        convertTime(local, target);
      }
      return target.getTime().getTime();
    }
  
    private static void convertTime(Calendar from, Calendar to) {
      to.set(Calendar.ERA, from.get(Calendar.ERA));
      to.set(Calendar.YEAR, from.get(Calendar.YEAR));
      to.set(Calendar.MONTH, from.get(Calendar.MONTH));
      to.set(Calendar.DAY_OF_MONTH, from.get(Calendar.DAY_OF_MONTH));
      to.set(Calendar.HOUR_OF_DAY, from.get(Calendar.HOUR_OF_DAY));
      to.set(Calendar.MINUTE, from.get(Calendar.MINUTE));
      to.set(Calendar.SECOND, from.get(Calendar.SECOND));
      to.set(Calendar.MILLISECOND, from.get(Calendar.MILLISECOND));
    }
  
    /**
     * Convert the timestamp using the specified calendar.
     * 
     * @param x
     *          the time
     * @param calendar
     *          the calendar
     * @return the timestamp
     */
    public static ValueTimestamp convertTimestamp(Timestamp x, Calendar calendar) {
      if (calendar == null) {
        throw JdbcException.getInvalidValueException("calendar", null);
      }
      Calendar cal = (Calendar) calendar.clone();
      cal.setTimeInMillis(x.getTime());
      long dateValue = dateValueFromCalendar(cal);
      long nanos = nanosFromCalendar(cal);
      nanos += x.getNanos() % 1000000;
      return ValueTimestamp.fromDateValueAndNanos(dateValue, nanos);
    }
  
    /**
     * Parse a date string. The format is: [+|-]year-month-day
     * 
     * @param s
     *          the string to parse
     * @param start
     *          the parse index start
     * @param end
     *          the parse index end
     * @return the date value
     * @throws IllegalArgumentException
     *           if there is a problem
     */
    public static long parseDateValue(String s, int start, int end) {
      if (s.charAt(start) == '+') {
        // +year
        start++;
      }
      // start at position 1 to support "-year"
      int s1 = s.indexOf('-', start + 1);
      int s2 = s.indexOf('-', s1 + 1);
      if (s1 <= 0 || s2 <= s1) {
        throw new IllegalArgumentException(s);
      }
      int year = Integer.parseInt(s.substring(start, s1));
      int month = Integer.parseInt(s.substring(s1 + 1, s2));
      int day = Integer.parseInt(s.substring(s2 + 1, end));
      if (!isValidDate(year, month, day)) {
        throw new IllegalArgumentException(year + "-" + month + "-" + day);
      }
      return dateValue(year, month, day);
    }
  
    /**
     * Parse a time string. The format is: [-]hour:minute:second[.nanos]
     * 
     * @param s
     *          the string to parse
     * @param start
     *          the parse index start
     * @param end
     *          the parse index end
     * @param timeOfDay
     *          whether the result need to be within 0 (inclusive) and 1 day
     *          (exclusive)
     * @return the time in nanoseconds
     * @throws IllegalArgumentException
     *           if there is a problem
     */
    public static long parseTimeNanos(String s, int start, int end,
        boolean timeOfDay) {
      int hour = 0, minute = 0, second = 0;
      long nanos = 0;
      int s1 = s.indexOf(':', start);
      int s2 = s.indexOf(':', s1 + 1);
      int s3 = s.indexOf('.', s2 + 1);
      if (s1 <= 0 || s2 <= s1) {
        throw new IllegalArgumentException(s);
      }
      boolean negative;
      hour = Integer.parseInt(s.substring(start, s1));
      if (hour < 0) {
        if (timeOfDay) {
          throw new IllegalArgumentException(s);
        }
        negative = true;
        hour = -hour;
      } else {
        negative = false;
      }
      minute = Integer.parseInt(s.substring(s1 + 1, s2));
      if (s3 < 0) {
        second = Integer.parseInt(s.substring(s2 + 1, end));
      } else {
        second = Integer.parseInt(s.substring(s2 + 1, s3));
        String n = (s.substring(s3 + 1, end) + "000000000").substring(0, 9);
        nanos = Integer.parseInt(n);
      }
      if (hour >= 2000000 || minute < 0 || minute >= 60 || second < 0
          || second >= 60) {
        throw new IllegalArgumentException(s);
      }
      if (timeOfDay && hour >= 24) {
        throw new IllegalArgumentException(s);
      }
      nanos += ((((hour * 60L) + minute) * 60) + second) * 1000000000;
      return negative ? -nanos : nanos;
    }
  
    /**
     * Calculate the milliseconds since 1970-01-01 (UTC) for the given date and
     * time (in the specified timezone).
     * 
     * @param tz
     *          the timezone of the parameters
     * @param year
     *          the absolute year (positive or negative)
     * @param month
     *          the month (1-12)
     * @param day
     *          the day (1-31)
     * @param hour
     *          the hour (0-23)
     * @param minute
     *          the minutes (0-59)
     * @param second
     *          the number of seconds (0-59)
     * @param millis
     *          the number of milliseconds
     * @return the number of milliseconds (UTC)
     */
    public static long getMillis(TimeZone tz, int year, int month, int day,
        int hour, int minute, int second, int millis) {
      try {
        return getTimeTry(false, tz, year, month, day, hour, minute, second,
            millis);
      } catch (IllegalArgumentException e) {
        // special case: if the time simply doesn't exist because of
        // daylight saving time changes, use the lenient version
        String message = e.toString();
        if (message.indexOf("HOUR_OF_DAY") > 0) {
          if (hour < 0 || hour > 23) {
            throw e;
          }
          return getTimeTry(true, tz, year, month, day, hour, minute, second,
              millis);
        } else if (message.indexOf("DAY_OF_MONTH") > 0) {
          int maxDay;
          if (month == 2) {
            maxDay = new GregorianCalendar().isLeapYear(year) ? 29 : 28;
          } else {
            maxDay = 30 + ((month + (month > 7 ? 1 : 0)) & 1);
          }
          if (day < 1 || day > maxDay) {
            throw e;
          }
          // DAY_OF_MONTH is thrown for years > 2037
          // using the timezone Brasilia and others,
          // for example for 2042-10-12 00:00:00.
          hour += 6;
          return getTimeTry(true, tz, year, month, day, hour, minute, second,
              millis);
        } else {
          return getTimeTry(true, tz, year, month, day, hour, minute, second,
              millis);
        }
      }
    }
  
    private static long getTimeTry(boolean lenient, TimeZone tz, int year,
        int month, int day, int hour, int minute, int second, int millis) {
      Calendar c;
      if (tz == null) {
        c = getCalendar();
      } else {
        c = Calendar.getInstance(tz);
      }
      synchronized (c) {
        c.clear();
        c.setLenient(lenient);
        setCalendarFields(c, year, month, day, hour, minute, second, millis);
        return c.getTime().getTime();
      }
    }
  
    private static void setCalendarFields(Calendar cal, int year, int month,
        int day, int hour, int minute, int second, int millis) {
      if (year <= 0) {
        cal.set(Calendar.ERA, GregorianCalendar.BC);
        cal.set(Calendar.YEAR, 1 - year);
      } else {
        cal.set(Calendar.ERA, GregorianCalendar.AD);
        cal.set(Calendar.YEAR, year);
      }
      // january is 0
      cal.set(Calendar.MONTH, month - 1);
      cal.set(Calendar.DAY_OF_MONTH, day);
      cal.set(Calendar.HOUR_OF_DAY, hour);
      cal.set(Calendar.MINUTE, minute);
      cal.set(Calendar.SECOND, second);
      cal.set(Calendar.MILLISECOND, millis);
    }
  
    /**
     * Get the specified field of a date, however with years normalized to
     * positive or negative, and month starting with 1.
     * 
     * @param d
     *          the date
     * @param field
     *          the field type
     * @return the value
     */
    public static int getDatePart(java.util.Date d, int field) {
      Calendar c = getCalendar();
      synchronized (c) {
        c.setTime(d);
        if (field == Calendar.YEAR) {
          return getYear(c);
        }
        int value = c.get(field);
        if (field == Calendar.MONTH) {
          return value + 1;
        }
        return value;
      }
    }
  
    /**
     * Get the year (positive or negative) from a calendar.
     * 
     * @param calendar
     *          the calendar
     * @return the year
     */
    private static int getYear(Calendar calendar) {
      int year = calendar.get(Calendar.YEAR);
      if (calendar.get(Calendar.ERA) == GregorianCalendar.BC) {
        year = 1 - year;
      }
      return year;
    }
  
    /**
     * Get the number of milliseconds since 1970-01-01 in the local timezone, but
     * without daylight saving time into account.
     * 
     * @param d
     *          the date
     * @return the milliseconds
     */
    public static long getTimeLocalWithoutDst(java.util.Date d) {
      return d.getTime() + zoneOffset;
    }
  
    /**
     * Convert the number of milliseconds since 1970-01-01 in the local timezone
     * to UTC, but without daylight saving time into account.
     * 
     * @param millis
     *          the number of milliseconds in the local timezone
     * @return the number of milliseconds in UTC
     */
    public static long getTimeUTCWithoutDst(long millis) {
      return millis - zoneOffset;
    }
  
    /**
     * Return the day of week according to the ISO 8601 specification. Week starts
     * at Monday. See also http://en.wikipedia.org/wiki/ISO_8601
     * 
     * @author Robert Rathsack
     * 
     * @param date
     *          the date object which day of week should be calculated
     * @return the day of the week, Monday as 1 to Sunday as 7
     */
    public static int getIsoDayOfWeek(java.util.Date date) {
      Calendar cal = Calendar.getInstance();
      cal.setTimeInMillis(date.getTime());
      int val = cal.get(Calendar.DAY_OF_WEEK) - 1;
      return val == 0 ? 7 : val;
    }
  
    /**
     * Returns the week of the year according to the ISO 8601 specification. The
     * spec defines the first week of the year as the week which contains at least
     * 4 days of the new year. The week starts at Monday. Therefore December 29th
     * - 31th could belong to the next year and January 1st - 3th could belong to
     * the previous year. If January 1st is on Thursday (or earlier) it belongs to
     * the first week, otherwise to the last week of the previous year. Hence
     * January 4th always belongs to the first week while the December 28th always
     * belongs to the last week.
     * 
     * @author Robert Rathsack
     * @param date
     *          the date object which week of year should be calculated
     * @return the week of the year
     */
    public static int getIsoWeek(java.util.Date date) {
      Calendar c = Calendar.getInstance();
      c.setTimeInMillis(date.getTime());
      c.setFirstDayOfWeek(Calendar.MONDAY);
      c.setMinimalDaysInFirstWeek(4);
      return c.get(Calendar.WEEK_OF_YEAR);
    }
  
    /**
     * Returns the year according to the ISO week definition.
     * 
     * @author Robert Rathsack
     * 
     * @param date
     *          the date object which year should be calculated
     * @return the year
     */
    public static int getIsoYear(java.util.Date date) {
      Calendar cal = Calendar.getInstance();
      cal.setTimeInMillis(date.getTime());
      cal.setFirstDayOfWeek(Calendar.MONDAY);
      cal.setMinimalDaysInFirstWeek(4);
      int year = getYear(cal);
      int month = cal.get(Calendar.MONTH);
      int week = cal.get(Calendar.WEEK_OF_YEAR);
      if (month == 0 && week > 51) {
        year--;
      } else if (month == 11 && week == 1) {
        year++;
      }
      return year;
    }
  
    /**
     * Formats a date using a format string.
     * 
     * @param date
     *          the date to format
     * @param format
     *          the format string
     * @param locale
     *          the locale
     * @param timeZone
     *          the timezone
     * @return the formatted date
     */
    public static String formatDateTime(java.util.Date date, String format,
        String locale, String timeZone) {
      SimpleDateFormat dateFormat = getDateFormat(format, locale, timeZone);
      synchronized (dateFormat) {
        return dateFormat.format(date);
      }
    }
  
    /**
     * Parses a date using a format string.
     * 
     * @param date
     *          the date to parse
     * @param format
     *          the parsing format
     * @param locale
     *          the locale
     * @param timeZone
     *          the timeZone
     * @return the parsed date
     */
    public static java.util.Date parseDateTime(String date, String format,
        String locale, String timeZone) {
      SimpleDateFormat dateFormat = getDateFormat(format, locale, timeZone);
      try {
        synchronized (dateFormat) {
          return dateFormat.parse(date);
        }
      } catch (Exception e) {
        // ParseException
        throw JdbcException.get(SQLErrorCode.PARSE_ERROR_1, e, date);
      }
    }
  
    private static SimpleDateFormat getDateFormat(String format, String locale,
        String timeZone) {
      try {
        // currently, a new instance is create for each call
        // however, could cache the last few instances
        SimpleDateFormat df;
        if (locale == null) {
          df = new SimpleDateFormat(format);
        } else {
          Locale l = new Locale(locale);
          df = new SimpleDateFormat(format, l);
        }
        if (timeZone != null) {
          df.setTimeZone(TimeZone.getTimeZone(timeZone));
        }
        return df;
      } catch (Exception e) {
        throw JdbcException.get(SQLErrorCode.PARSE_ERROR_1, e, format + "/" + locale
            + "/" + timeZone);
      }
    }
  
    /**
     * Verify if the specified date is valid.
     * 
     * @param year
     *          the year
     * @param month
     *          the month (January is 1)
     * @param day
     *          the day (1 is the first of the month)
     * @return true if it is valid
     */
    public static boolean isValidDate(int year, int month, int day) {
      if (month < 1 || month > 12 || day < 1) {
        return false;
      }
      if (year > 1582) {
        // Gregorian calendar
        if (month != 2) {
          return day <= NORMAL_DAYS_PER_MONTH[month];
        }
        // February
        if ((year & 3) != 0) {
          return day <= 28;
        }
        return day <= ((year % 100 != 0) || (year % 400 == 0) ? 29 : 28);
      } else if (year == 1582 && month == 10) {
        // special case: days 1582-10-05 .. 1582-10-14 don't exist
        return day <= 31 && (day < 5 || day > 14);
      }
      if (month != 2 && day <= NORMAL_DAYS_PER_MONTH[month]) {
        return true;
      }
      return day <= ((year & 3) != 0 ? 28 : 29);
    }
  
    /**
     * Convert a date value to a date, using the default timezone.
     * 
     * @param dateValue
     *          the date value
     * @return the date
     */
    public static Date convertDateValueToDate(long dateValue) {
      long millis = getMillis(TimeZone.getDefault(),
          yearFromDateValue(dateValue), monthFromDateValue(dateValue),
          dayFromDateValue(dateValue), 0, 0, 0, 0);
      return new Date(millis);
    }
  
    /**
     * Convert a date value / time value to a timestamp, using the default
     * timezone.
     * 
     * @param dateValue
     *          the date value
     * @param nanos
     *          the nanoseconds since midnight
     * @return the timestamp
     */
    public static Timestamp convertDateValueToTimestamp(long dateValue, long nanos) {
      long millis = nanos / 1000000;
      nanos -= millis * 1000000;
      long s = millis / 1000;
      millis -= s * 1000;
      long m = s / 60;
      s -= m * 60;
      long h = m / 60;
      m -= h * 60;
      long ms = getMillis(TimeZone.getDefault(), yearFromDateValue(dateValue),
          monthFromDateValue(dateValue), dayFromDateValue(dateValue), (int) h,
          (int) m, (int) s, 0);
      Timestamp ts = new Timestamp(ms);
      ts.setNanos((int) (nanos + millis * 1000000));
      return ts;
    }
  
    /**
     * Convert a time value to a time, using the default timezone.
     * 
     * @param nanos
     *          the nanoseconds since midnight
     * @return the time
     */
    public static Time convertNanoToTime(long nanos) {
      long millis = nanos / 1000000;
      long s = millis / 1000;
      millis -= s * 1000;
      long m = s / 60;
      s -= m * 60;
      long h = m / 60;
      m -= h * 60;
      long ms = getMillis(TimeZone.getDefault(), 1970, 1, 1, (int) (h % 24),
          (int) m, (int) s, (int) millis);
      return new Time(ms);
    }
  
    /**
     * Get the year from a date value.
     * 
     * @param x
     *          the date value
     * @return the year
     */
    public static int yearFromDateValue(long x) {
      return (int) (x >>> SHIFT_YEAR);
    }
  
    /**
     * Get the month from a date value.
     * 
     * @param x
     *          the date value
     * @return the month (1..12)
     */
    public static int monthFromDateValue(long x) {
      return (int) (x >>> SHIFT_MONTH) & 15;
    }
  
    /**
     * Get the day of month from a date value.
     * 
     * @param x
     *          the date value
     * @return the day (1..31)
     */
    public static int dayFromDateValue(long x) {
      return (int) (x & 31);
    }
  
    /**
     * Get the date value from a given date.
     * 
     * @param year
     *          the year
     * @param month
     *          the month (1..12)
     * @param day
     *          the day (1..31)
     * @return the date value
     */
    public static long dateValue(long year, int month, int day) {
      return (year << SHIFT_YEAR) | (month << SHIFT_MONTH) | day;
    }
  
    /**
     * Calculate the date value (in the default timezone) from a given time in
     * milliseconds in UTC.
     * 
     * @param ms
     *          the milliseconds
     * @return the date value
     */
    public static long dateValueFromDate(long ms) {
      Calendar cal = getCalendar();
      synchronized (cal) {
        cal.clear();
        cal.setTimeInMillis(ms);
        return dateValueFromCalendar(cal);
      }
    }
  
    /**
     * Calculate the date value from a given calendar.
     * 
     * @param cal
     *          the calendar
     * @return the date value
     */
    private static long dateValueFromCalendar(Calendar cal) {
      int year, month, day;
      year = getYear(cal);
      month = cal.get(Calendar.MONTH) + 1;
      day = cal.get(Calendar.DAY_OF_MONTH);
      return ((long) year << SHIFT_YEAR) | (month << SHIFT_MONTH) | day;
    }
  
    /**
     * Calculate the nanoseconds since midnight (in the default timezone) from a
     * given time in milliseconds in UTC.
     * 
     * @param ms
     *          the milliseconds
     * @return the nanoseconds
     */
    public static long nanosFromDate(long ms) {
      Calendar cal = getCalendar();
      synchronized (cal) {
        cal.clear();
        cal.setTimeInMillis(ms);
        return nanosFromCalendar(cal);
      }
    }
  
    /**
     * Calculate the nanoseconds since midnight from a given calendar.
     * 
     * @param cal
     *          the calendar
     * @return the nanoseconds
     */
    private static long nanosFromCalendar(Calendar cal) {
      int h = cal.get(Calendar.HOUR_OF_DAY);
      int m = cal.get(Calendar.MINUTE);
      int s = cal.get(Calendar.SECOND);
      int millis = cal.get(Calendar.MILLISECOND);
      return ((((((h * 60L) + m) * 60) + s) * 1000) + millis) * 1000000;
    }
  
    /**
     * Calculate the normalized timestamp.
     * 
     * @param absoluteDay
     *          the absolute day
     * @param nanos
     *          the nanoseconds (may be negative or larger than one day)
     * @return the timestamp
     */
    public static ValueTimestamp normalizeTimestamp(long absoluteDay, long nanos) {
      if (nanos > NANOS_PER_DAY || nanos < 0) {
        long d;
        if (nanos > NANOS_PER_DAY) {
          d = nanos / NANOS_PER_DAY;
        } else {
          d = (nanos - NANOS_PER_DAY + 1) / NANOS_PER_DAY;
        }
        nanos -= d * NANOS_PER_DAY;
        absoluteDay += d;
      }
      return ValueTimestamp.fromDateValueAndNanos(
          dateValueFromAbsoluteDay(absoluteDay), nanos);
    }
  
    /**
     * Calculate the absolute day from a date value.
     * 
     * @param dateValue
     *          the date value
     * @return the absolute day
     */
    public static long absoluteDayFromDateValue(long dateValue) {
      long y = yearFromDateValue(dateValue);
      int m = monthFromDateValue(dateValue);
      int d = dayFromDateValue(dateValue);
      if (m <= 2) {
        y--;
        m += 12;
      }
      long a = ((y * 2922L) >> 3) + DAYS_OFFSET[m - 3] + d - 719484;
      if (y <= 1582 && ((y < 1582) || (m * 100 + d < 1005))) {
        // Julian calendar (cutover at 1582-10-04 / 1582-10-15)
        a += 13;
      } else if (y < 1901 || y > 2099) {
        // Gregorian calendar (slow mode)
        a += (y / 400) - (y / 100) + 15;
      }
      return a;
    }
  
    /**
     * Calculate the date value from an absolute day.
     * 
     * @param absoluteDay
     *          the absolute day
     * @return the date value
     */
    public static long dateValueFromAbsoluteDay(long absoluteDay) {
      long d = absoluteDay + 719468;
      long y100 = 0, offset;
      if (d > 578040) {
        // Gregorian calendar
        long y400 = d / 146097;
        d -= y400 * 146097;
        y100 = d / 36524;
        d -= y100 * 36524;
        offset = y400 * 400 + y100 * 100;
      } else {
        // Julian calendar
        d += 292200000002L;
        offset = -800000000;
      }
      long y4 = d / 1461;
      d -= y4 * 1461;
      long y = d / 365;
      d -= y * 365;
      if (d == 0 && (y == 4 || y100 == 4)) {
        y--;
        d += 365;
      }
      y += offset + y4 * 4;
      // month of a day
      int m = ((int) d * 2 + 1) * 5 / 306;
      d -= DAYS_OFFSET[m] - 1;
      if (m >= 10) {
        y++;
        m -= 12;
      }
      return dateValue(y, m + 3, (int) d);
    }
  
  }
  