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

com.squarespace.cldrengine.calendars.CalendarFormatter Maven / Gradle / Ivy

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

import static com.squarespace.cldrengine.utils.StringUtils.isEmpty;

import com.squarespace.cldrengine.api.Bundle;
import com.squarespace.cldrengine.api.CalendarDate;
import com.squarespace.cldrengine.api.ContextTransformFieldType;
import com.squarespace.cldrengine.api.DayPeriodAltType;
import com.squarespace.cldrengine.api.Decimal;
import com.squarespace.cldrengine.api.EraAltType;
import com.squarespace.cldrengine.api.EraWidthType;
import com.squarespace.cldrengine.api.MetaZoneType;
import com.squarespace.cldrengine.api.TimeZoneNameType;
import com.squarespace.cldrengine.general.GeneralInternals;
import com.squarespace.cldrengine.internal.AbstractValue;
import com.squarespace.cldrengine.internal.CalendarFields;
import com.squarespace.cldrengine.internal.CalendarSchema;
import com.squarespace.cldrengine.internal.Internals;
import com.squarespace.cldrengine.internal.TimeZoneSchema;
import com.squarespace.cldrengine.internal.Vector2Arrow;
import com.squarespace.cldrengine.internal.Vector3Arrow;
import com.squarespace.cldrengine.parsing.DateTimePattern;
import com.squarespace.cldrengine.parsing.DateTimePattern.DateTimeNode;

import lombok.AllArgsConstructor;

/**
 * Formats a date-time pattern using a given calendar context.
 */
class CalendarFormatter {

  public final Internals internals;
  public final GeneralInternals general;
  public final CalendarSchema cal;
  public final TimeZoneSchema tz;

  public CalendarFormatter(Internals internals, CalendarSchema schema) {
    this.internals = internals;
    this.general = internals.general;
    this.cal = schema;
    this.tz = internals.schema.TimeZones;
  }

  public  void format(AbstractValue val, CalendarContext ctx, DateTimePattern pattern) {
    format(val, ctx, pattern, true);
  }

  public  void format(AbstractValue val, CalendarContext ctx, DateTimePattern pattern, boolean first) {
    int len = pattern.nodes.size();
    for (int i = 0; i < len; i++) {
      Object node = pattern.nodes.get(i);
      if (node instanceof String) {
        val.add("literal", (String)node);
        continue;
      }

      DateTimeNode d = (DateTimeNode)node;
      int w = d.width;
      ContextTransformFieldType field = null;
      String type = "";
      String value = "";

      // Date field symbol table
      // https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
      switch (d.field) {

        // ERA
        case 'G':
          type = "era";
          value = this.cal.eras.get(ctx.bundle,
              w == 5 ? EraWidthType.NARROW : w == 4 ? EraWidthType.NAMES : EraWidthType.ABBR,
                  Long.toString(ctx.date.era()),
                  new EraAltType[] { ctx.alt.era.get(), EraAltType.NONE });
          if (w != 5) {
            field = w == 4 ? ContextTransformFieldType.ERA_NAME : ContextTransformFieldType.ERA_ABBR;
          }
          break;

        // YEAR
        case 'y':
          type = "year";
          value = _year(ctx, ctx.date.year(), w);
          break;

        // YEAR IN WEEK OF YEAR
        case 'Y':
          type = "year";
          value = _year(ctx, ctx.date.yearOfWeekOfYear(), w);
          break;

        // EXTENDED YEAR
        case 'u':
          type = "year";
          value = _num(ctx, ctx.date.extendedYear(), w);
          break;

        // CYCLIC YEAR
        case 'U':
          type = "cyclic-year";
          // TODO: support chinese cyclical years
          value = "";
          break;

        // RELATED YEAR
        case 'r':
          type = "related-year";
          // Note: this is always rendered using 'latn' digits
          value = ctx.latnSystem.formatString(ctx.date.relatedYear(), false, w);
          break;

        // QUARTER
        case 'Q':
        case 'q':
          type = "quarter";
          value = this.quarter(ctx, d);
          break;

        // MONTH FORMAT
        case 'M':
          type = "month";
          value = this.month(ctx, d);
          switch (w) {
            case 3:
            case 4:
              field = ContextTransformFieldType.MONTH_FORMAT_EXCEPT_NARROW;
              break;
          }
          break;

         // MONTH STANDALONE
        case 'L':
          type = "month";
          value = this.month(ctx, d);
          switch (w) {
            case 3:
            case 4:
              field = ContextTransformFieldType.MONTH_STANDALONE_EXCEPT_NARROW;
              break;
          }
          break;


        // 'l' - deprecated

        // WEEK OF WEEK YEAR
        case 'w':
          type = "week";
          value = _num(ctx, ctx.date.weekOfYear(), Math.min(w, 2));
          break;

        // WEEK OF MONTH
        case 'W':
          type = "week";
          value = _num(ctx, ctx.date.weekOfMonth(), 1);
          break;

        // DAY OF MONTH
        case 'd':
          type = "day";
          value = _num(ctx, ctx.date.dayOfMonth(), Math.min(w,  2));
          break;

        // DAY OF YEAR
        case 'D':
          type = "day";
          value = _num(ctx, ctx.date.dayOfYear(), Math.min(w,  3));
          break;

        // DAY OF WEEK IN MONTH
        case 'F':
          type = "day";
          value = _num(ctx, ctx.date.dayOfWeekInMonth(), 1);
          break;

        // MODIFIED JULIAN DAY
        case 'g':
          type = "mjulian-day";
          value = _num(ctx, ctx.date.modifiedJulianDay(), w);
          break;

        // WEEKDAY FORMAT
        case 'E':
          type = "weekday";
          value = this._weekday(ctx.bundle, this.cal.format.weekdays, ctx.date, w);
          if (w != 5) {
            field = ContextTransformFieldType.DAY_FORMAT_EXCEPT_NARROW;
          }
          break;

        // WEEKDAY LOCAL
        case 'e':
          type = "weekday";
          value = this._weekdayLocal(ctx, d, false);
          break;

        // WEEKDAY LOCAL STANDALONE
        case 'c':
          type = "weekday";
          value = this._weekdayLocal(ctx, d, true);
          if (w != 5) {
            field = ContextTransformFieldType.DAY_STANDALONE_EXCEPT_NARROW;
          }
          break;

        // DAY PERIOD AM/PM
        case 'a':
          type = "dayperiod";
          value = this.cal.format.dayPeriods.get(ctx.bundle, widthKey(w),
              ctx.date.hourOfDay() < 12 ? "am" : "pm", new DayPeriodAltType[] { ctx.alt.dayPeriod.get(), DayPeriodAltType.NONE });
          break;

        // DAY PERIOD EXTENDED
        case 'b':
          type = "dayperiod";
          value = this.dayPeriodExt(ctx, d);
          break;

        // DAY PERIOD FLEXIBLE
        case 'B':
          type = "dayperiod";
          value = this.dayPeriodFlex(ctx, d);
          break;

        // HOUR 1-12 AND 0-23
        case 'h':
        case 'H':
          type = "hour";
          value = this.hour(ctx, d);
          break;

        // HOUR 0-11 and 1-24
        case 'K':
        case 'k':
          type = "hour";
          value = this.hourAlt(ctx, d);
          break;

        // 'j', 'J', 'C' - input skeleton symbols, not present in formats

        // MINUTE
        case 'm':
          type = "minute";
          value = _num(ctx, ctx.date.minute(), Math.min(w, 2));
          break;

        // SECOND
        case 's':
          type = "second";
          value = _num(ctx, ctx.date.second(), Math.min(w,  2));
          break;

        // FRACTIONAL SECOND
        case 'S':
          type = "fracsec";
          value = this.fractionalSecond(ctx, d);
          break;

        // MILLISECONDS IN DAY
        case 'A':
          type = "millis-in-day";
          value = _num(ctx, ctx.date.millisecondsInDay(), w);
          break;

        // TIMEZONE SPECIFIC NON-LOCATION
        case 'z':
          type = "timezone";
          value = this.timezone_z(ctx, d);
          break;

        // TIMEZONE ISO-8601 EXTENDED
        case 'Z':
          type = "timezone";
          value = this.timezone_Z(ctx, d);
          break;

        // TIMEZONE LOCALIZED
        case 'O':
          type = "timezone";
          value = this.timezone_O(ctx, d);
          break;

        // TIMEZONE GENERIC NON-LOCATION
        case 'v':
          type = "timezone";
          value = this.timezone_v(ctx, d);
          break;

        // TIMEZONE ID, EXEMPLAR CITY, GENERIC LOCATION
        case 'V':
          type = "timezone";
          value = this.timezone_V(ctx, d);
          break;

        // TIMEZONE ISO-8601 BASIC, EXTENDED
        case 'X':
        case 'x':
          type = "timezone";
          value = this.timezone_x(ctx, d);
          break;

        default:
          continue;
      }

      if (first && i == 0 && ctx.context != null && field != null) {
        value = this.internals.general.contextTransform(value, ctx.transform, ctx.context, field);
      }
      val.add(type, value);
    }
  }

  protected String _formatQuarterOrMonth(CalendarContext ctx, Vector2Arrow format,
      long value, int width) {
    return width >= 3 ?
        format.get(ctx.bundle, widthKey(width), Long.toString(value)) :
        _num(ctx, value, width);
  }

  protected String quarter(CalendarContext ctx, DateTimeNode node) {
    CalendarFields format = node.field == 'Q' ? this.cal.format : this.cal.standAlone;
    int quarter = (int)((ctx.date.month() - 1) / 3) + 1;
    return this._formatQuarterOrMonth(ctx, format.quarters, quarter, node.width);
  }

  protected String month(CalendarContext ctx, DateTimeNode node) {
    CalendarFields format = node.field == 'M' ? this.cal.format : this.cal.standAlone;
    return this._formatQuarterOrMonth(ctx, format.months, ctx.date.month(), node.width);
  }

  protected String _weekday(Bundle bundle, Vector2Arrow format, CalendarDate date, int width) {
    String key2 = Long.toString(date.dayOfWeek());
    String key1 = "abbreviated";
    switch (width) {
      case 6:
        key1 = "short";
        break;
      case 5:
        key1 = "narrow";
        break;
      case 4:
        key1 = "wide";
        break;
    }
    return format.get(bundle, key1, key2);
  }

  protected String _weekdayLocal(CalendarContext ctx, DateTimeNode node, boolean standalone) {
    int width = node.width;
    if (width > 2) {
      CalendarFields format = standalone ? this.cal.standAlone : this.cal.format;
      return this._weekday(ctx.bundle, format.weekdays, ctx.date, width);
    }
    long ord = ctx.date.ordinalDayOfWeek();
    if (standalone) {
      width = 1;
    }
    return ctx.system.formatString(ord, false, width);
  }

  protected String dayPeriodExt(CalendarContext ctx, DateTimeNode node) {
    String key1 = widthKey(node.width);
    String key2 = ctx.date.isAM() ? "am" : "pm";
    String key2ext = key2;
    if (ctx.date.minute() == 0) {
      long hour = ctx.date.hourOfDay();
      key2ext = hour == 0 ? "midnight" : hour == 12 ? "noon" : key2;
    }
    Vector3Arrow format = this.cal.format.dayPeriods;
    DayPeriodAltType[] alt = new DayPeriodAltType[] { ctx.alt.dayPeriod.get(), DayPeriodAltType.NONE };
    // Try extended and if it doesn't exist, fall back to am/pm
    String result = format.get(ctx.bundle, key1, key2ext, alt);
    return result.equals("") ? format.get(ctx.bundle, key1, key2, alt) : result;
  }

  protected String dayPeriodFlex(CalendarContext ctx, DateTimeNode node) {
    long minutes = (ctx.date.hourOfDay() * 60) + ctx.date.minute();
    String key = this.internals.calendars.flexDayPeriod(ctx.bundle, minutes);
    String res = null;
    if (key != null) {
      DayPeriodAltType[] alt = new DayPeriodAltType[] { ctx.alt.dayPeriod.get(), DayPeriodAltType.NONE };
      res = this.cal.format.dayPeriods.get(ctx.bundle, widthKey(node.width), key, alt);
    }
    return isEmpty(res) ? this.dayPeriodExt(ctx, node) : res;
  }

  protected String hour(CalendarContext ctx, DateTimeNode node) {
    boolean twelve = node.field == 'h';
    long hour = twelve ? ctx.date.hour() : ctx.date.hourOfDay();
    if (twelve && hour == 0) {
      hour = 12;
    }
    return _num(ctx, hour, Math.min(node.width, 2));
  }

  protected String hourAlt(CalendarContext ctx, DateTimeNode node) {
    boolean twelve = node.field == 'K';
    long hour = twelve ? ctx.date.hour() : ctx.date.hourOfDay();
    if (!twelve && hour == 0) {
      hour = 24;
    }
    return _num(ctx, hour, Math.min(node.width, 2));
  }

  protected String fractionalSecond(CalendarContext ctx, DateTimeNode node) {
    int width = node.width;
    long m = ctx.date.milliseconds();
    int d = width > 3 ? width - 3 : 0;
    width -= d;
    if (d > 0) {
      m *= Math.pow(10, d);
    }
    // Milliseconds always have precision 3, so handle the cases compactly
    long n = width == 3 ? m : (width == 2 ? (m / 10) : (m / 100));
    return _num(ctx, n, node.width);
  }

  /**
   * Timezone: short/long specific non-location format.
   * https://www.unicode.org/reports/tr35/tr35-dates.html#dfst-zone
   */
  protected String timezone_z(CalendarContext ctx, DateTimeNode node) {
    if (node.width > 4) {
      return "";
    }
    String key2 = ctx.date.metaZoneId();
    if (!isEmpty(key2)) {
      Vector2Arrow format = node.width == 4 ?
          this.tz.metaZones.long_ : this.tz.metaZones.short_;
      String name = format.get(ctx.bundle, ctx.date.isDaylightSavings() ? TimeZoneNameType.DAYLIGHT : TimeZoneNameType.STANDARD,
          MetaZoneType.fromString(key2));
      if (!isEmpty(name)) {
        return name;
      }
    }
    return this.timezone_O(ctx, node);
  }

  /**
   * Timezone: ISO8601 basic/extended format, long localized GMT format.
   * https://www.unicode.org/reports/tr35/tr35-dates.html#dfst-zone
   */
  protected String timezone_Z(CalendarContext ctx, DateTimeNode node) {
    int width = node.width;
    if (width == 4) {
      return this.timezone_O(ctx, new DateTimeNode('O', width));
    }

    TZC tzc = getTZC(ctx.date.timeZoneOffset());
    StringBuilder fmt = new StringBuilder();
    if (width <= 5) {
      // TODO: use number params
      fmt.append(tzc.negative ? '-' : '+');
      fmt.append(_num(ctx, tzc.hours, 2));
      if (width == 5) {
        fmt.append(':');
      }
      fmt.append(_num(ctx, tzc.minutes, 2));
    }
    return fmt.toString();
  }

  /**
   * Timezone: short/long localized GMT format.
   */
  protected String timezone_O(CalendarContext ctx, DateTimeNode node) {
    return node.width == 1 || node.width == 4 ? this._wrapGMT(ctx, node.width == 1) : "";
  }

  /**
   * Timezone: short/long generic non-location format.
   */
  protected String timezone_v(CalendarContext ctx, DateTimeNode node) {
    int width = node.width;
    if (width != 1 && width != 4) {
      return "";
    }
    String name = "";
    String key = ctx.date.metaZoneId();
    Vector2Arrow format = width == 1 ?
        this.tz.metaZones.short_ : this.tz.metaZones.long_;
    name = format.get(ctx.bundle, TimeZoneNameType.GENERIC, MetaZoneType.fromString(key));
    return name.isEmpty() ? this.timezone_O(ctx, new DateTimeNode('O', width)) : name;
  }

  /**
   * Timezone: short/long zone ID, exemplar city, generic location format.
   * https://www.unicode.org/reports/tr35/tr35-dates.html#dfst-zone
   */
  protected String timezone_V(CalendarContext ctx, DateTimeNode node) {
    String stableId = ctx.date.timeZoneStableId();
    String city = "";
    switch (node.width) {
      case 4:
        city = this.tz.exemplarCity.get(ctx.bundle, stableId);
        if (city.isEmpty()) {
          return this.timezone_O(ctx, new DateTimeNode('O', 4));
        }
        String pattern = this.tz.regionFormat.get(ctx.bundle);
        return this.general.formatWrapper(pattern, city);

      case 3:
        // Exemplar city for the timezone
        city = this.tz.exemplarCity.get(ctx.bundle, stableId);
        return city.isEmpty() ? this.tz.exemplarCity.get(ctx.bundle, "Etc/Unknown") : city;

      case 2:
        return ctx.date.timeZoneId();

      case 1:
        return "unk";

      default:
        return "";
    }
  }

  /**
   * Timezone: ISO8601 basic format
   * https://www.unicode.org/reports/tr35/tr35-dates.html#dfst-zone
   */
  protected String timezone_x(CalendarContext ctx, DateTimeNode node) {
    TZC tzc = getTZC(ctx.date.timeZoneOffset());
    StringBuilder fmt = new StringBuilder();
    if (node.width >= 1 && node.width <= 5) {
      boolean zero = tzc.hours == 0 && tzc.minutes == 0;
      fmt.append(zero ? '+' : tzc.negative ? '-' : '+');
      fmt.append(_num(ctx, tzc.hours, 2));
      if (node.width == 3 || node.width == 5) {
        fmt.append(':');
      }
      if (node.width != 1 || tzc.minutes > 0) {
        fmt.append(_num(ctx, tzc.minutes, 2));
      }
      if (node.field == 'X' && tzc.offset == 0) {
        fmt.append('Z');
      }
    }
    return fmt.toString();
  }

  protected String _wrapGMT(CalendarContext ctx, boolean short_) {
    long offset = ctx.date.timeZoneOffset();
    if (offset == 0) {
      return this.tz.gmtZeroFormat.get(ctx.bundle);
    }
    TZC tzc = getTZC(offset);
    boolean emitMins = !short_ || tzc.minutes > 0;
    DateTimePattern hourPattern = this._hourPattern(ctx.bundle, tzc.negative);
    StringBuilder fmt = new StringBuilder();
    for (int i = 0; i < hourPattern.nodes.size(); i++) {
      Object elem = hourPattern.nodes.get(i);
      if (elem instanceof String) {
        String p = (String)elem;
        boolean sep = p.equals(".") || p.equals(":");
        if (!sep || emitMins) {
          fmt.append(p);
        }
      } else {
        DateTimeNode node = (DateTimeNode)elem;
        if (node.field == 'H') {
          fmt.append(node.width == 1 ? _num(ctx, tzc.hours, 1) : _num(ctx, tzc.hours, short_ ? 1 : node.width));
        } else if (node.field == 'm' && emitMins) {
          fmt.append(_num(ctx, tzc.minutes, node.width));
        }
      }
    }

    String wrap = this.tz.gmtFormat.get(ctx.bundle);
    return this.general.formatWrapper(wrap, fmt.toString());
  }

  protected DateTimePattern _hourPattern(Bundle bundle, boolean negative) {
    String raw = this.tz.hourFormat.get(bundle);
    return this.internals.calendars.getHourPattern(raw, negative);
  }

  private String _num(CalendarContext ctx, long n, int minInt) {
    return ctx.system.formatString(new Decimal(n), false, minInt);
  }

  private String _year(CalendarContext ctx, long year, int width) {
    return _num(ctx, width == 2 ? year % 100 : year, width);
  }

  private static String widthKey(int width) {
    switch (width) {
      case 5:
        return "narrow";
      case 4:
        return "wide";
      default:
        return "abbreviated";
    }
  }

  private static TZC getTZC(long offset) {
    boolean negative = offset < 0;
    if (negative) {
      offset *= -1;
    }
    offset /= 60000;
    long hours = offset / 60;
    long minutes = offset % 60;
    return new TZC(offset, negative, hours, minutes);
  }

  @AllArgsConstructor
  private static class TZC {
    long offset;
    boolean negative;
    long hours;
    long minutes;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy