All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.squarespace.cldrengine.api.CalendarDate Maven / Gradle / Ivy

The newest version!
package com.squarespace.cldrengine.api;


import static com.squarespace.cldrengine.api.Pair.of;

import java.util.Arrays;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;

import com.squarespace.cldrengine.calendars.DayOfWeek;
import com.squarespace.cldrengine.calendars.TimeZoneData;
import com.squarespace.cldrengine.calendars.ZoneInfo;
import com.squarespace.cldrengine.internal.DateTimePatternFieldType;
import com.squarespace.cldrengine.internal.MathFix;

import lombok.AllArgsConstructor;

public abstract class CalendarDate {

  private static final long NULL = Long.MAX_VALUE;

  private static final List> DIFFERENCE_FIELDS = Arrays.asList(
      of(DateField.YEAR, DateTimePatternFieldType.YEAR),
      of(DateField.MONTH, DateTimePatternFieldType.MONTH),
      of(DateField.DAY_OF_MONTH, DateTimePatternFieldType.DAY),
      of(DateField.AM_PM, DateTimePatternFieldType.DAYPERIOD),
      of(DateField.HOUR, DateTimePatternFieldType.HOUR),
      of(DateField.MINUTE, DateTimePatternFieldType.MINUTE)
      );

  protected final long[] fields = new long[DateField.LENGTH];
  protected final CalendarType type;
  protected final int firstDay;
  protected final int minDays;
  protected ZoneInfo zoneInfo; // set in subclass

  protected CalendarDate(CalendarType type, int firstDay, int minDays) {
    this.type = type;
    this.firstDay = firstDay;
    this.minDays = minDays;

    // Compute week fields on demand.
    this.fields[DateField.WEEK_OF_YEAR] = NULL;
    this.fields[DateField.YEAR_WOY] = NULL;
  }

  /**
   * Calendar type for this date, e.g. 'gregory' for Gregorian.
   */
  public CalendarType type() {
    return this.type;
  }

  /**
   * Unix epoch with no timezone offset.
   */
  public long unixEpoch() {
    return this.fields[DateField.LOCAL_MILLIS] - this.zoneInfo.offset;
  }


  public int firstDayOfWeek() {
    return this.firstDay;
  }

  public int minDaysInFirstWeek() {
    return this.minDays;
  }

  /**
   * Returns a floating point number representing the real Julian Day, UTC.
   */
  public double julianDay() {
    double ms = (this.fields[DateField.MILLIS_IN_DAY] + this.zoneInfo.offset) / (double)CalendarConstants.ONE_DAY_MS;
    return (this.fields[DateField.JULIAN_DAY] - 0.5) + ms;
  }

  /**
   * CLDR's modified Julian day used as the basis for all date calculations.
   */
  public long modifiedJulianDay() {
    return this.fields[DateField.JULIAN_DAY];
  }

  public long era() {
    return this.fields[DateField.ERA];
  }

  public long extendedYear() {
    return this.fields[DateField.EXTENDED_YEAR];
  }

  public long year() {
    return this.fields[DateField.YEAR];
  }

  public long relatedYear() {
    return this.fields[DateField.EXTENDED_YEAR];
  }

  public long yearOfWeekOfYear()  {
    this.computeWeekFields();
    return this.fields[DateField.YEAR_WOY];
  }

  public long weekOfYear() {
    this.computeWeekFields();
    return this.fields[DateField.WEEK_OF_YEAR];
  }

  public long yearOfWeekOfYearISO() {
    this.computeWeekFields();
    return this.fields[DateField.ISO_YEAR_WOY];
  }

  public long weekOfYearISO() {
    this.computeWeekFields();
    return this.fields[DateField.ISO_WEEK_OF_YEAR];
  }

  /**
   * Ordinal month, one-based, e.g. Gregorian JANUARY = 1.
   */
  public long month() {
    return this.fields[DateField.MONTH];
  }

  /**
   * Returns the week of the month computed using the locale's 'first day
   * of week' and 'minimal days in first week' where applicable.
   *
   * For example, for the United States, weeks start on Sunday.
   * Saturday 9/1/2018 would be in week 1, and Sunday 9/2/2018 would
   * begin week 2.
   *
   *         September
   *   Su Mo Tu We Th Fr Sa
   *                      1
   *    2  3  4  5  6  7  8
   *    9 10 11 12 13 14 15
   *   16 17 18 19 20 21 22
   *   23 24 25 26 27 28 29
   *   30
   */
  public long weekOfMonth() {
    this.computeWeekFields();
    return this.fields[DateField.WEEK_OF_MONTH];
  }

  public long dayOfYear() {
    return this.fields[DateField.DAY_OF_YEAR];
  }

  /**
   * Day of the week. 1 = SUNDAY, 2 = MONDAY, ..., 7 = SATURDAY
   */
  public long dayOfWeek() {
    return this.fields[DateField.DAY_OF_WEEK];
  }

  /**
   * Ordinal day of the week. 1 if this is the 1st day of the week,
   * 2 if the 2nd, etc. Depends on the local starting day of the week.
   */
  public long ordinalDayOfWeek() {
    long weekday = this.dayOfWeek();
    long firstDay = this.firstDayOfWeek();
    return (7 - firstDay + weekday) % 7 + 1;
  }

  /**
   * Ordinal number indicating the day of the week in the current month.
   * The result of this method can be used to format messages like
   * "2nd Sunday in August".
   */
  public long dayOfWeekInMonth() {
    this.computeWeekFields();
    return this.fields[DateField.DAY_OF_WEEK_IN_MONTH];
  }

  public long dayOfMonth() {
    return this.fields[DateField.DAY_OF_MONTH];
  }

  public boolean isAM() {
    return this.fields[DateField.AM_PM] == 0;
  }

  /**
   * Indicates the hour of the morning or afternoon, used for the 12-hour
   * clock (0 - 11). Noon and midnight are 0, not 12.
   */
  public long hour() {
    return this.fields[DateField.HOUR];
  }

  /**
   * Indicates the hour of the day, used for the 24-hour clock (0 - 23).
   * Noon is 12 and midnight is 0.
   */
  public long hourOfDay() {
    return this.fields[DateField.HOUR_OF_DAY];
  }

  /**
   * Indicates the minute of the hour (0 - 59).
   */
  public long minute() {
    return this.fields[DateField.MINUTE];
  }

  /**
   * Indicates the second of the minute (0 - 59).
   */
  public long second() {
    return this.fields[DateField.SECOND];
  }

  public long milliseconds() {
    return this.fields[DateField.MILLIS];
  }

  public long millisecondsInDay() {
    return this.fields[DateField.MILLIS_IN_DAY];
  }

  public String metaZoneId() {
    return this.zoneInfo.metaZoneId;
  }

  public String timeZoneId() {
    return this.zoneInfo.timeZoneId;
  }

  public String timeZoneStableId() {
    return this.zoneInfo.stableId;
  }

  public int timeZoneOffset() {
    return this.zoneInfo.offset;
  }

  public String timeZoneAbbr() {
    return this.zoneInfo.abbr;
  }

  public boolean isLeapYear() {
    return this.fields[DateField.IS_LEAP] == 1;
  }

  public boolean isDaylightSavings() {
    return this.zoneInfo.dst;
  }

  /**
   * Computes the field of visual difference between the two dates.
   * Note: This assumes the dates are of the same type and have the same
   * timezone offset.
   */
  public DateTimePatternFieldType fieldOfVisualDifference(CalendarDate other) {
    long[] a = this.fields;
    long[] b = other.fields;
    for (Pair pair : DIFFERENCE_FIELDS) {
      int key = pair._1;
      if (a[key] != b[key]) {
        return pair._2;
      }
    }
    return DateTimePatternFieldType.SECOND;
  }

  /**
   * Compare two dates a and b, returning:
   *   a < b  ->  -1
   *   a = b  ->  0
   *   a > b  ->  1
   */
  public int compare(CalendarDate other) {
    long a = this.unixEpoch();
    long b = other.unixEpoch();
    return a < b ? -1 : a > b ? 1 : 0;
  }

  /**
   * Calculate the relative time between two dates. If a field is specified
   * the time will be calculated in terms of that single field. Otherwise
   * the field of greatest difference will be used.
   */
  public Pair relativeTime(CalendarDate other, TimePeriodField field) {
    Swap swap = this.swap(other);
    TimePeriod diff = this._diff(swap.start, swap.startFields, swap.endFields);
    TimePeriodField _field = field;
    if (_field == null) {
      _field = largestRelativeField(diff);
    }
    TimePeriod res = this._rollup(diff, swap.startFields, swap.endFields, Arrays.asList(_field));
    Double value = getRelativeField(res, _field);
    return Pair.of(_field, value);
  }

  /**
   * Calculate the time period between two dates. Note this returns the
   * absolute value.
   */
  public TimePeriod difference(CalendarDate other, List fields) {
    Swap swap = this.swap(other);
    TimePeriod res = this._diff(swap.start, swap.startFields, swap.endFields);
    if (fields != null) {
      return this._rollup(res, swap.startFields, swap.endFields, fields);
    }
    return res;
  }

  public abstract CalendarDate add(TimePeriod fields);
  public abstract CalendarDate subtract(TimePeriod fields);
  public abstract CalendarDate withZone(String zoneId);

  protected abstract void initFields(long[] f);
  protected abstract int monthCount();
  protected abstract int daysInMonth(long year, int month);
  protected abstract int daysInYear(long year);
  protected abstract long monthStart(long eyear, double month, boolean useMonth);

  protected TimePeriodField largestRelativeField(TimePeriod p) {
    if (p.year.ok() && p.year.get() != 0.0) {
      return TimePeriodField.YEAR;
    }
    if (p.month.ok() && p.month.get() != 0.0) {
      return TimePeriodField.MONTH;
    }
    if (p.week.ok() && p.week.get() != 0.0) {
      return TimePeriodField.WEEK;
    }
    if (p.day.ok() && p.day.get() != 0.0) {
      return TimePeriodField.DAY;
    }
    if (p.hour.ok() && p.hour.get() != 0.0) {
      return TimePeriodField.HOUR;
    }
    if (p.minute.ok() && p.minute.get() != 0.0) {
      return TimePeriodField.MINUTE;
    }
    if (p.second.ok() && p.second.get() != 0.0) {
      return TimePeriodField.SECOND;
    }
    return TimePeriodField.MILLIS;
  }

  protected double getRelativeField(TimePeriod period, TimePeriodField field) {
    switch (field) {
      case YEAR:
        return period.year.or(0.0);
      case MONTH:
        return period.month.or(0.0);
      case WEEK:
        return period.week.or(0.0);
      case DAY:
        return period.day.or(0.0);
      case HOUR:
        return period.hour.or(0.0);
      case MINUTE:
        return period.minute.or(0.0);
      case SECOND:
        return period.second.or(0.0);
      default:
        return period.millis.or(0.0);
    }
  }

  protected TimePeriod invertPeriod(TimePeriod f) {
    TimePeriod r = new TimePeriod();
    r.year(invert(f.year));
    r.month(invert(f.month));
    r.week(invert(f.week));
    r.day(invert(f.day));
    r.hour(invert(f.hour));
    r.minute(invert(f.minute));
    r.second(invert(f.second));
    r.millis(invert(f.millis));
    return r;
  }

  private Double invert(Option d) {
    if (d.ok()) {
      double v = d.get();
      return v == 0 ? v : -v;
    }
    return null;
  }

  @AllArgsConstructor
  protected static class Swap {
    CalendarDate start;
    long[] startFields;
    CalendarDate end;
    long[] endFields;
  }

  protected Swap swap(CalendarDate other) {
    CalendarDate start = this;
    CalendarDate end = other;
    if (this.compare(other) == 1) {
      CalendarDate tmp = start;
      start = end;
      end = tmp;
    }
    return new Swap(start, start.utcfields(), end, end.utcfields());
  }

  /**
   * Roll up time period fields into a subset of fields.
   */
  protected TimePeriod _rollup(TimePeriod span, long[] sf, long[] ef, List fields) {
    int f = timePeriodFieldFlags(fields);
    if (f == 0) {
      return span;
    }

    int mc = this.monthCount();

    double year = span.year.or(0.0);
    double month = span.month.or(0.0);
    double day = (span.week.or(0.0) * 7) + span.day.or(0.0);
    double ms = (span.hour.or(0.0) * CalendarConstants.ONE_HOUR_MS) +
        (span.minute.or(0.0) * CalendarConstants.ONE_MINUTE_MS) +
        (span.second.or(0.0) * CalendarConstants.ONE_SECOND_MS) +
        span.millis.or(0.0);

    if (((f & FLAG_YEAR) != 0) && ((f & FLAG_MONTH) != 0)) {
      // Both year and month were requested, so use their integer values.

    } else if ((f & FLAG_MONTH) != 0) {
      // Month was requested so convert years into months
      month += year * mc;
      year = 0;

    } else if ((f & FLAG_YEAR) != 0 && month > 0) {
      // Year was requested so convert months into days

      // This is a little more verbose but necessary to accurately convert
      // months into days. Example:
      //
      //  2001-03-11  and 2001-09-09   5 months and 29 days apart
      //  == (last month days) + (full month days) + (first month days)
      //  == 9 + 31 + 31 + 30 + 31 + 30 + (31 - 11)
      //  == 182 days

      long endy = ef[DateField.EXTENDED_YEAR];
      long endm = ef[DateField.MONTH] - 1;

      // TODO: create a cursor for year/month calculations to reduce
      // the verbosity of this block

      // Subtract the number of days to find the "day of month"
      // relative to each of the months to be converted.
      double dom = ef[DateField.DAY_OF_MONTH] - day;
      if (dom < 0) {
        endm--;
        if (endm < 0) {
          endm += mc;
          endy--;
        }
        dom += this.daysInMonth(endy, (int)endm);
      }

      // Convert each month except the last into days
      double tmpd = dom;
      while (month > 1) {
        endm--;
        if (endm < 0) {
          endm += mc;
          endy--;
        }
        tmpd += this.daysInMonth(endy, (int)endm);
        month--;
      }

      // Convert the last month into days
      endm--;
      if (endm < 0) {
        endm += mc;
        endy--;
      }

      tmpd += this.daysInMonth(endy, (int)endm) - dom;
      day += tmpd;
      month = 0;

    } else {
      // Neither year nor month were requested, so we ignore those parts
      // of the time period, and re-calculate the days directly from the
      // original date fields.
      day = ef[DateField.JULIAN_DAY] - sf[DateField.JULIAN_DAY];
      ms = ef[DateField.MILLIS_IN_DAY] - sf[DateField.MILLIS_IN_DAY];
      if (ms < 0) {
        day--;
        ms += CalendarConstants.ONE_DAY_MS;
      }
      year = month = 0;
    }

    // We have integer year, month, and millis computed at this point

    ms += CalendarConstants.ONE_DAY_MS * day;
    day = 0;

    long onedy = CalendarConstants.ONE_DAY_MS;
    long onewk = onedy * 7;
    long onehr = CalendarConstants.ONE_HOUR_MS;
    long onemn = CalendarConstants.ONE_MINUTE_MS;

    double week = 0;
    double hour = 0;
    double minute = 0;
    double second = 0;
    double millis = 0;

    // Roll down
    if ((f & FLAG_WEEK) != 0) {
      week = Math.floor(ms / onewk);
      ms -= week * onewk;
    }
    if ((f & FLAG_DAY) != 0) {
      day = Math.floor(ms / onedy);
      ms -= day * onedy;
    }
    if ((f & FLAG_HOUR) != 0) {
      hour = Math.floor(ms / onehr);
      ms -= hour * onehr;
    }
    if ((f & FLAG_MINUTE) != 0) {
      minute = Math.floor(ms / onemn);
      ms -= minute * onemn;
    }
    if ((f & FLAG_SECOND) != 0) {
      second = Math.floor(ms / 1000);
      ms -= second * 1000;
    }
    if ((f & FLAG_MILLIS) != 0) {
      millis = ms;
    }

    double dayms = ms / CalendarConstants.ONE_DAY_MS;

    // Roll up fractional
    if (f < FLAG_MONTH) {
      // Days in the last year before adding the remaining fields
      long diy = this.daysInYear((long) (sf[DateField.EXTENDED_YEAR] + year));
      year += (day + dayms) / diy;
      day = 0;
    } else if (f < FLAG_WEEK) {
      long ey = ef[DateField.YEAR];
      long em = ef[DateField.MONTH] - 2;
      if (em < 0) {
        em += mc;
        ey--;
      }
      int dim = this.daysInMonth(ey, (int)em);
      month += (day + dayms) / dim;
    } else if (f < FLAG_DAY) {
      week += (day + dayms) / 7;
    } else if (f < FLAG_HOUR) {
      day += dayms;
    } else if (f < FLAG_MINUTE) {
      hour += ms / onehr;
    } else if (f < FLAG_SECOND) {
      minute += ms / onemn;
    } else if (f < FLAG_MILLIS) {
      second += ms / 1000;
    }

    return TimePeriod.build()
        .year(year)
        .month(month)
        .week(week)
        .day(day)
        .hour(hour)
        .minute(minute)
        .second(second)
        .millis(millis);
  }

  /**
   * Compute the number of years, months, days, etc, between two dates. The result will
   * have all fields as integers.
   */
  protected TimePeriod _diff(CalendarDate s, long[] sf, long[] ef) {
    // Use a borrow-based method to compute fields. If a field X is negative, we borrow
    // from the next-higher field until X is positive. Repeat until all fields are
    // positive.
    long millis = ef[DateField.MILLIS_IN_DAY] - sf[DateField.MILLIS_IN_DAY];
    long day = ef[DateField.DAY_OF_MONTH] - sf[DateField.DAY_OF_MONTH];
    long month = ef[DateField.MONTH] - sf[DateField.MONTH];
    long year = ef[DateField.EXTENDED_YEAR] - sf[DateField.EXTENDED_YEAR];

    // Convert days into milliseconds
    if (millis < 0) {
      millis += CalendarConstants.ONE_DAY_MS;
      day--;
    }

    // Convert months into days
    // This is a little more complex since months can have 28, 29 30 or 31 days.
    // We work backwards from the current month and successively convert months
    // into days until days are positive.
    int mc = s.monthCount();
    long m = ef[DateField.MONTH] - 1; // convert to 0-based month
    long y = ef[DateField.EXTENDED_YEAR];
    while (day < 0) {
      // move to previous month
      m--;
      // add back the number of days in the current month, wrapping around to December
      if (m < 0) {
        m += mc;
        y--;
      }
      int dim = this.daysInMonth(y, (int)m);
      day += dim;
      month--;
    }

    // Convert years into months
    if (month < 0) {
      month += mc;
      year--;
    }

    // Convert days to weeks
    long week = day > 0 ? day / 7 : 0;
    if (week > 0) {
      day -= week * 7;
    }

    // Break down milliseconds into components
    long hour = millis / CalendarConstants.ONE_HOUR_MS;
    millis -= hour * CalendarConstants.ONE_HOUR_MS;
    long minute = millis / CalendarConstants.ONE_MINUTE_MS;
    millis -= minute * CalendarConstants.ONE_MINUTE_MS;
    long second = millis / CalendarConstants.ONE_SECOND_MS;
    millis -= second * CalendarConstants.ONE_SECOND_MS;

    return TimePeriod.build()
        .year((double)year)
        .month((double)month)
        .week((double)week)
        .day((double)day)
        .hour((double)hour)
        .minute((double)minute)
        .second((double)second)
        .millis((double)millis);
  }

  protected int timePeriodFieldFlags(List fields) {
    int flags = 0;
    for (TimePeriodField field : fields) {
      flags |= FIELDMAP.get(field);
    }
    return flags;
  }

  private static final Map FIELDMAP = new EnumMap<>(TimePeriodField.class);

  private static final int FLAG_YEAR = 1;
  private static final int FLAG_MONTH = 2;
  private static final int FLAG_WEEK = 4;
  private static final int FLAG_DAY = 8;
  private static final int FLAG_HOUR = 16;
  private static final int FLAG_MINUTE = 32;
  private static final int FLAG_SECOND = 64;
  private static final int FLAG_MILLIS = 128;

  static {
    FIELDMAP.put(TimePeriodField.YEAR, FLAG_YEAR);
    FIELDMAP.put(TimePeriodField.MONTH, FLAG_MONTH);
    FIELDMAP.put(TimePeriodField.WEEK, FLAG_WEEK);
    FIELDMAP.put(TimePeriodField.DAY, FLAG_DAY);
    FIELDMAP.put(TimePeriodField.HOUR, FLAG_HOUR);
    FIELDMAP.put(TimePeriodField.MINUTE, FLAG_MINUTE);
    FIELDMAP.put(TimePeriodField.SECOND, FLAG_SECOND);
    FIELDMAP.put(TimePeriodField.MILLIS, FLAG_MILLIS);
  }

  /**
   * Compute a new Julian day and milliseconds UTC by updating one or more fields.
   */
  protected Pair _add(TimePeriod fields) {
    long[] f = this.utcfields();

    long jd;
    double ms;
    long year;
    double yearf;

    long ydays;
    double ydaysf;

    long month;
    double monthf;

    long day;
    double dayf;

    // Capture days and time fields (in milliseconds) for future use.
    // We do this here since we'll be re-initializing the date fields below.
    Pair daysms = this._addTime(fields);
    double _days = daysms._1;
    double _ms = daysms._2;
    _days += fields.day.or(0.0) + (fields.week.or(0.0) * 7);

    // YEARS

    // Split off the fractional part of the years. Add the integer
    // years to the extended year. Then get the number of days in that
    // year and multiply that by the fractional part.
    // Example: In a Gregorian leap year we'll have 366 days. If the fractional
    // year is 0.25 we'll get 91.5 days.
    Pair split = splitfrac(fields.year.or(0.0));
    year = split._1;
    yearf = split._2;
    year += f[DateField.EXTENDED_YEAR];

    split = splitfrac(this.daysInYear(year) * yearf);
    ydays = split._1;
    ydaysf = split._2;

    // Add day fractions from year calculation to milliseconds
    ms = ydaysf * CalendarConstants.ONE_DAY_MS;

    // Calculate the julian day for the year, month and day-of-month combination,
    // adding in the days due to fractional year
    jd = this.monthStart(year, f[DateField.MONTH] - 1, false) + f[DateField.DAY_OF_MONTH] + ydays;

    // Initialize fields from the julian day
    f[DateField.JULIAN_DAY] = jd;
    f[DateField.MILLIS_IN_DAY] = 0;
    this.initFields(f);

    year = f[DateField.EXTENDED_YEAR];

    // MONTHS

    // Get integer and fractional months
    split = splitfrac((f[DateField.MONTH] - 1) + fields.month.or(0.0));
    month = split._1;
    monthf = split._2;

    // Add back years by dividing by month count
    int mc = this.monthCount();
    split = splitfrac(month / 12); // ignore fraction here
    long myears = split._1;
    month -= myears * mc;
    year += myears;

    // Take away a year if the month pointer went negative
    if (month < 0) {
      month += mc;
      year--;
    }

    // Compute updated julian day from year and fractional month
    double dim = this.daysInMonth(year, (int)month) * monthf;
    split = splitfrac(_days + dim);
    day = split._1;
    dayf = split._2;
    jd = this.monthStart(year, month, false) + f[DateField.DAY_OF_MONTH];

    // DAY AND TIME FIELDS

    // Adjust julian day by fractional day and time fields
    ms += Math.round(_ms + (dayf * CalendarConstants.ONE_DAY_MS));
    if (ms >= CalendarConstants.ONE_DAY_MS) {
      double d = Math.floor(ms / CalendarConstants.ONE_DAY_MS);
      ms -= d * CalendarConstants.ONE_DAY_MS;
      day += d;
    }
    return Pair.of(jd + day, ms);
  }

  /**
   * Converts all time fields into [days, milliseconds].
   */
  protected Pair _addTime(TimePeriod fields) {
    double msDay = this.fields[DateField.MILLIS_IN_DAY] - this.timeZoneOffset();
    msDay += (fields.hour.or(0.0) * CalendarConstants.ONE_HOUR_MS) +
        (fields.minute.or(0.0) * CalendarConstants.ONE_MINUTE_MS) +
        (fields.second.or(0.0) * CalendarConstants.ONE_SECOND_MS) +
        fields.millis.or(0.0);
    long days = (long) Math.floor(msDay / CalendarConstants.ONE_DAY_MS);
    double ms = msDay - (days * CalendarConstants.ONE_DAY_MS);
    return Pair.of(days, ms);
  }

  protected Pair splitfrac(double n) {
    double t = Math.abs(n);
    int sign = n < 0 ? -1 : 1;
    long r = (long)Math.floor(t);
    return Pair.of(sign * r, sign * (t - r));
  }

  protected void initFromUnixEpoch(long ms, String zoneId) {
    zoneId = TimeZoneData.substituteZoneAlias(zoneId);
    this.zoneInfo = TimeZoneData.zoneInfoFromUTC(zoneId, ms);
    jdFromUnixEpoch(ms + this.zoneInfo.offset, this.fields);
    computeBaseFields(this.fields);
  }

  protected void initFromJD(long jd, long msDay, String zoneId) {
    long unixEpoch = unixEpochFromJD(jd, msDay);
    this.initFromUnixEpoch(unixEpoch, zoneId);
  }

  protected String _toString(String type) {
    long year = this.extendedYear();
    boolean neg = year < 0;
    return String.format("%s %s%04d-%02d-%02d %02d:%02d:%02d.%03d %s",
        type,
        neg ? "-" : "",
        Math.abs(year),
        this.month(),
        this.dayOfMonth(),
        this.hourOfDay(),
        this.minute(),
        this.second(),
        this.milliseconds(),
        this.zoneInfo.timeZoneId);
  }

  /**
   * Compute WEEK_OF_YEAR and YEAR_WOY on demand.
   */
  protected void computeWeekFields() {
    long[] f = this.fields;
    if (f[DateField.YEAR_WOY] != NULL) {
      return;
    }

    long dow = f[DateField.DAY_OF_WEEK];
    long dom = f[DateField.DAY_OF_MONTH];
    long doy = f[DateField.DAY_OF_YEAR];
    f[DateField.WEEK_OF_MONTH] = weekNumber(this.firstDay, this.minDays,  dom, dom, dow);
    f[DateField.DAY_OF_WEEK_IN_MONTH] = ((dom - 1) / 7 | 0) + 1;

    // compute locale
    this._computeWeekFields(DateField.WEEK_OF_YEAR, DateField.YEAR_WOY, this.firstDay, this.minDays, dow, dom, doy);

    // compute ISO
    this._computeWeekFields(DateField.ISO_WEEK_OF_YEAR, DateField.ISO_YEAR_WOY, 2, 4, dow, dom, doy);
  }

  protected void _computeWeekFields(int woyfield, int ywoyfield, long firstDay, long minDays, long dow, long dom, long doy) {
    long[] f = this.fields;
    long eyear = f[DateField.EXTENDED_YEAR];

    long ywoy = eyear;
    long rdow = (dow + 7 - firstDay) % 7;
    long rdowJan1 = (dow - doy + 7001 - firstDay) % 7;
    long woy = MathFix.floorDiv((doy - 1 + rdowJan1), 7);
    if (7 - rdowJan1 >= minDays) {
      woy++;
    }

    if (woy == 0) {
      long prevDay = doy + this.yearLength(eyear - 1);
      woy = weekNumber(firstDay, minDays, prevDay, prevDay, dow);
      ywoy--;
    } else {
      long lastDoy = this.yearLength(eyear);
      if (doy >= lastDoy - 5) {
        long lastRdow = (rdow + lastDoy - doy) % 7;
        if (lastRdow < 0) {
          lastRdow += 7;
        }
        if (6 - lastRdow >= minDays && doy + 7 - rdow > lastDoy) {
          woy = 1;
          ywoy++;
        }
      }
    }
    f[woyfield] = woy;
    f[ywoyfield] = ywoy;
  }

  protected long yearLength(long y) {
    return this.monthStart(y + 1, 0, false) - this.monthStart(y, 0, false);
  }

  protected static long weekNumber(long firstDay, long minDays, long desiredDay, long dayOfPeriod, long dayOfWeek) {
    long psow = (dayOfWeek - firstDay - dayOfPeriod + 1) % 7;
    if (psow < 0) {
      psow += 7;
    }
    long weekNo = MathFix.floorDiv(desiredDay + psow - 1, 7);
    return ((7 - psow) >= minDays) ? weekNo + 1 : weekNo;
  }

  protected long[] utcfields() {
    long u = this.unixEpoch();
    long[] f = Arrays.copyOf(this.fields, this.fields.length);
    jdFromUnixEpoch(u, f);
    computeBaseFields(f);
    initFields(f);
    return f;
  }

  /**
   * Compute Julian day from timezone-adjusted Unix epoch milliseconds.
   */
  protected void jdFromUnixEpoch(long ms, long[] f) {
    long oneDayMS = CalendarConstants.ONE_DAY_MS;
    long days = MathFix.floorDiv(ms, oneDayMS);
    long jd = days + CalendarConstants.JD_UNIX_EPOCH;
    long msDay = ms - (days * oneDayMS);

    f[DateField.JULIAN_DAY] = jd;
    f[DateField.MILLIS_IN_DAY] = msDay;
  }

  /**
   * Given a Julian day and local milliseconds (in UTC), return the Unix
   * epoch milliseconds UTC.
   */
  protected long unixEpochFromJD (long jd, long msDay) {
    long days = jd - CalendarConstants.JD_UNIX_EPOCH;
    return (days * CalendarConstants.ONE_DAY_MS) + msDay;
  }

  /**
   * Compute fields common to all calendars. Before calling this, we must
   * have the JULIAN_DAY and MILLIS_IN_DAY fields set. Every calculation
   * is relative to these.
   */
  protected void computeBaseFields(long[] f) {
    long jd = clamp(f[DateField.JULIAN_DAY], CalendarConstants.JD_MIN, CalendarConstants.JD_MAX);
//    checkJDRange(jd);

    long msDay = f[DateField.MILLIS_IN_DAY];
    long ms = msDay + ((jd - CalendarConstants.JD_UNIX_EPOCH) * CalendarConstants.ONE_DAY_MS);

    f[DateField.LOCAL_MILLIS] = ms;
    f[DateField.JULIAN_DAY] = jd;
    f[DateField.MILLIS_IN_DAY] = msDay;
    f[DateField.MILLIS] = msDay % 1000;

    msDay = msDay / 1000;
    f[DateField.SECOND] = msDay % 60;

    msDay = msDay / 60;
    f[DateField.MINUTE] = msDay % 60;

    msDay = msDay / 60;
    f[DateField.HOUR_OF_DAY] = msDay;
    f[DateField.AM_PM] = msDay / 12;
    f[DateField.HOUR] = msDay % 12;

    long dow = (jd + DayOfWeek.MONDAY) % 7;
    if (dow < DayOfWeek.SUNDAY) {
      dow += 7;
    }
    f[DateField.DAY_OF_WEEK] = dow;
  }

  private static long clamp(long n, long min, long max) {
    return n < min ? min : (n > max ? max : n);
  }

//  protected long checkJDRange(long jd) {
//    // TODO: emit warning?
//
////  throw new Error(
////  `Julian day ${jd} is outside the supported range of this library: ` +
////  `${ConstantsDesc.JD_MIN} to ${ConstantsDesc.JD_MAX}`);
//
//    if (jd < CalendarConstants.JD_MIN) {
//      return CalendarConstants.JD_MIN;
//    }
//    return jd > CalendarConstants.JD_MAX ? CalendarConstants.JD_MAX : jd;
//  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy