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

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

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

import static com.squarespace.cldrengine.utils.JsonUtils.decodeArray;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.squarespace.cldrengine.internal.TimeZoneExternalData;
import com.squarespace.cldrengine.utils.JsonUtils;
import com.squarespace.cldrengine.utils.Search;
import com.squarespace.cldrengine.utils.StringUtils;

public class TimeZoneData {

  // Mapping of additional aliases to canonical timezone identifier
  private static final Map ZONEALIASES = new HashMap<>();

  // Mapping of CLDR stable timezone identifiers
  private static final Map ZONE_TO_STABLEID = new HashMap<>();

  // Set to check if a timezone id is a CLDR stable id
  private static final Set CLDR_STABLEIDS = new HashSet<>();

  // Maps a canonical timezone identifier to the index offset its zone info
  private static final Map ZONEINDEX = new HashMap<>();

  // Map aliases and lowercase forms to canonical timezone identifier
  private static final Map LINKINDEX = new HashMap<>();

  // Map a timezone identifier to a metazone index
  private static final Map ZONETOMETAZONE = new HashMap<>();

  // Static mapping of characters to integer
  private static Map TYPES = new HashMap<>();

  // Default UTC zone here for quick access.
  private static final TZInfo UTC = new TZInfo("Etc/UTC", "UTC", 0, 0);

  // Array of decoded timezone identifiers
  private static String[] TIMEZONEIDS;

  // Array of until timestamps sorted by usage frequency
  private static long[] UNTILINDEX;

  // Decoded zoneinfo records
  private static ZoneRecord[] ZONERECORDS;

  // Array of metazone identifiers
  private static String[] METAZONEIDS;

  // Decoded metazone records
  private static MetazoneRecord[] METAZONES;

  static {
    encodeTypes();
    loadStableIds();
    loadAliases();
    loadTimezones();
    loadMetazones();
  }

  /**
   * Maps a possible timezone alias to the correct id.
   */
  public static String substituteZoneAlias(String id) {
    String zoneId = ZONEALIASES.get(id);
    return zoneId == null ? id : zoneId;
  }

  /**
   * Lookup the zoneinfo record for the given timezone id and UTC timestamp.
   */
  public static ZoneInfo zoneInfoFromUTC(String zoneId, long utc) {
    TZInfo info = lookup(zoneId, utc, true);
    if (info == null) {
      info = UTC;
    }

    // For the purposes of CLDR stable timezone ids, check if the passed-in
    // id is an alias to a current/valid tzdb id.
    boolean isstable = CLDR_STABLEIDS.contains(zoneId);

    // Use the passed-in id as the stable id if it is an alias,
    // otherwise lookup the id in the stable map.
    String stableId = isstable ? zoneId : getStableId(zoneId);

    String metazoneId = getMetazone(info.zoneId, utc);
    return new ZoneInfo(
      info.zoneId,
      stableId,
      metazoneId == null ? "" : metazoneId,
      info.abbr,
      info.offset,
      info.dst == 1
    );
  }

  public static boolean zoneIsStable(String zoneId) {
    return CLDR_STABLEIDS.contains(zoneId);
  }

  /**
   * Metadata related to a zone, such as the list of country codes that overlap with
   * the zone, the latitude and longitude, and the current standard offset, in milliseconds.
   * These can be used to display user interfaces for selecting a zone.
   *
   * If the zone identifier does not match a known zone or alias this returns null.
   */
  public static ZoneMeta zoneMeta(String id) {
    ZoneRecord rec = record(id);
    if (rec != null) {
      return new ZoneMeta(rec.zoneId, rec.stdoff, rec.latitude, rec.longitude, rec.countries);
    }
    return null;
  }

  /**
   * For a given timezone identifier and UTC timestamp, return the
   * metazone identifier or null if none exists.
   */
  public static String getMetazone(String zoneId, long utc) {
    Integer i = ZONETOMETAZONE.get(zoneId);
    if (i != null) {
      MetazoneRecord rec = METAZONES[i];
      if (rec != null) {
        // Note: we don't bother with binary search here since the metazone
        // until arrays are quite short.
        int len = rec.untils.length;
        for (int j = len - 1; j > 0; j--) {
          if (rec.untils[j] <= utc) {
            return METAZONEIDS[(int)rec.offsets[j]];
          }
        }

        // Hit the end, return the initial metazone id
        return METAZONEIDS[(int)rec.offsets[0]];
      }
    }

    // This zone has no metazone id, e.g. "Etc/GMT+1"
    return null;
  }

  /**
   * Get the info for a time zone using a UTC timestamp.
   */
  public static TZInfo fromUTC(String zoneId, long utc) {
    return lookup(zoneId, utc, true);
  }

  /**
   * UTC zone info.
   */
  public static TZInfo utcZone() {
    return TimeZoneData.UTC;
  }

  /**
   * Resolve a lowercase time zone id or alias into the canonical proper-cased id.
   */
  public static String resolveId(String id) {
    return LINKINDEX.get(id);
  }

  /**
   * Returns a list of time zone ids.
   */
  public static List zoneIds() {
    return Arrays.asList(TIMEZONEIDS);
  }

  /**
   * Map a timezone identifier to the CLDR stable id
   */
  public static String getStableId(String id) {
    return ZONE_TO_STABLEID.getOrDefault(id, id);
  }

  private static TZInfo lookup(String id, long utc, boolean isUTC) {
    ZoneRecord rec = record(id);
    return rec == null ? null : rec.fromUTC(utc);
  }

  private static ZoneRecord record(String zoneId) {
    String id = LINKINDEX.get(zoneId);
    if (id == null) {
      return null;
    }

    // Find the offset to the record for this zone. This should
    // always be non-null.
    int i = ZONEINDEX.get(id);
    return ZONERECORDS[i];
  }

  /**
   * Encode a mapping of a character to its integer offset for decoding
   * the tzdb type values.
   */
  private static void encodeTypes() {
    String[] parts = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
    for (int i = 0; i < parts.length; i++) {
      TYPES.put(parts[i], i);
    }
  }

  /**
   * Load the mapping of timezone alias to canonical identifier.
   */
  private static void loadAliases() {
    for (String row : TimeZoneExternalData.ZONEALIASRAW.split("\\|")) {
      String[] parts = row.split(":");
      ZONEALIASES.put(parts[0], parts[1]);
    }
  }

  /**
   * Load all timezone data.
   */
  private static void loadTimezones() {
    JsonObject root;
    try {
      root = JsonUtils.loadJsonResource(TimeZoneExternalData.class, "zonedata.json");
    } catch (IOException e) {
      throw new RuntimeException("Failed to load timezone data resource", e);
    }

    // Decode timezone ids and index array position
    TIMEZONEIDS = split(root.get("zoneids"), "\\|");
    for (int i = 0; i < TIMEZONEIDS.length; i++) {
      String id = TIMEZONEIDS[i];
      ZONEINDEX.put(id, i);
      addLink(id, id);
    }

    // Decode links
    String[] parts = split(root.get("links"), "\\|");
    for (String part : parts) {
      String[] kv = part.split(":");
      String alias = kv[0];
      String id = TIMEZONEIDS[Integer.parseInt(kv[1])];
      addLink(alias, id);
    }

    // Decode timezone until index and raw zone info
    UNTILINDEX = StringUtils.longArray(root.get("index").getAsString());

    // Decode all zoneinfo records
    String[] rawzoneinfo = decodeArray(root.get("zoneinfo"));
    ZONERECORDS = new ZoneRecord[rawzoneinfo.length];
    for (int i = 0; i < rawzoneinfo.length; i++) {
      ZONERECORDS[i] = new ZoneRecord(TIMEZONEIDS[i], rawzoneinfo[i]);
    }
  }

  /**
   * Load CLDR metazone records and CLDR stable identifiers.
   *
   * Example: "America_Eastern" represents a metazone that
   * references standard and daylight savings identifiers for
   * "America/New_York", etc.
   *
   * A CLDR stable timezone identifier never changes, even as
   * canonical TZDB identifiers are deprecated. Each time a
   * new TZDB is released we generate the mappings back to the
   * corresponding CLDR stable identifier, where they differ.
   */
  private static void loadMetazones() {
    JsonObject root = (JsonObject) JsonUtils.parse(TimeZoneExternalData.METAZONEDATA);
    METAZONEIDS = decodeArray(root.get("metazoneids"));

    long[] index = StringUtils.longArray(root.get("index").getAsString());
    long[] offsets = StringUtils.longArray(root.get("offsets").getAsString());
    long[] untils = StringUtils.longArray(root.get("untils").getAsString());

    // Decode all metazone records
    METAZONES = new MetazoneRecord[index.length / 2];
    for (int i = 0; i < index.length; i += 2) {
      int start = (int)index[i];
      int end = (int)index[i + 1];
      METAZONES[i / 2] = new MetazoneRecord(
        Arrays.copyOfRange(offsets, start, end),
        Arrays.copyOfRange(untils, start, end)
      );
    }

    // Map timezone identifiers to corresponding metazone record offset
    long[] zoneindex = StringUtils.longArray(root.get("zoneindex").getAsString());
    for (int i = 0; i < zoneindex.length; i++) {
      int mi = (int) zoneindex[i];
      if (mi != -1) {
        ZONETOMETAZONE.put(TIMEZONEIDS[i], mi);
        ZONETOMETAZONE.put(TIMEZONEIDS[i].toLowerCase(), mi);
      }
    }

    // Map timezone identifier back to CLDR stable identifier
    String[] parts = split(root.get("stableids"), "\\|");
    for (int i = 0; i < parts.length; i++) {
      String[] kv = parts[i].split(":");
      String zoneid = TIMEZONEIDS[Integer.parseInt(kv[0])];
      ZONE_TO_STABLEID.put(zoneid, kv[1]);
    }
  }

  /**
   * Load the set of CLDR stable timezone ids.
   */
  private static void loadStableIds() {
    String[] ids = decodeArray(JsonUtils.parse(TimeZoneExternalData.STABLEIDS));
    for (String id : ids) {
      CLDR_STABLEIDS.add(id);
    }
  }

  private static void addLink(String src, String dst) {
    LINKINDEX.put(src, dst);
    LINKINDEX.put(src.toLowerCase(), dst);
  }

  private static String[] split(JsonElement elem, String delim) {
    return split(elem.getAsString(), delim);
  }

  private static String[] split(String raw, String delim) {
    return raw.isEmpty() ? new String[] { } : raw.split(delim);
  }

  /**
   * Holds paired until timestamps and offsets used to determine
   * which metazone identifier to use at a given point in time.
   */
  private static class MetazoneRecord {

    final long[] offsets;
    final long[] untils;

    public MetazoneRecord(long[] offsets, long[] untils) {
      this.offsets = offsets;
      this.untils = untils;
    }

    @Override
    public String toString() {
      return "MetazoneRecord(offsets=" + Arrays.toString(this.offsets)
        + " untils=" + Arrays.toString(this.untils)
        + ")";
    }
  }

  /**
   * Record for a single timezone, used to determine which localtime
   * record is in effect at a given point in time.
   */
  private static class ZoneRecord {
    final String zoneId;
    final long stdoff;
    final double latitude;
    final double longitude;
    final String[] countries;

    final TZInfo[] localtime;
    final int[] types;
    final long[] untils;

    public ZoneRecord(String zoneId, String raw) {
      this.zoneId = zoneId;
      String[] parts = split(raw, "_");
      String _std = parts[0];
      String _lat = parts[1];
      String _lon = parts[2];
      String _countries = parts[3];
      String _info = parts[4];
      String _types = parts.length > 5 ? parts[5] : "";
      String _untils = parts.length > 6 ? parts[6] : "";

      long[] untils = StringUtils.longArray(_untils);

      int len = untils.length;
      if (len > 0) {
        untils[0] = UNTILINDEX[(int)untils[0]] * 1000;
        for (int i = 1; i < len; i++) {
          untils[i] = untils[i - 1] + (UNTILINDEX[(int)untils[i]] * 1000);
        }
      }

      parts = split(_info, "\\|");
      this.localtime = new TZInfo[parts.length];
      for (int i = 0; i < parts.length; i++) {
        this.localtime[i] = TZInfo.decode(zoneId, parts[i]);
      }

      parts = split(_types, "");
      this.types = new int[parts.length];
      for (int i = 0; i < parts.length; i++) {
        this.types[i] = TYPES.get(parts[i]);
      }
      this.untils = untils;

      this.stdoff = Long.parseLong(_std, 36) * 1000;
      this.latitude = Long.parseLong(_lat, 36) / 1e6;
      this.longitude = Long.parseLong(_lon, 36) / 1e6;
      this.countries = _countries.isEmpty() ? new String[] {} : _countries.split(",");
    }

    public TZInfo fromUTC(long utc) {
      int i = Search.binarySearch(this.untils, true, utc);
      int type = i == -1 ? 0 : this.types[i];
      return this.localtime[type];
    }

    @Override
    public String toString() {
      return "ZoneRecord(untils=" + Arrays.toString(untils)
        + " localtime=" + Arrays.toString(localtime)
        + " types=" + Arrays.toString(types)
        + ")";
    }
  }

  /**
   * Holds a timezone abbreviation, daylight savings (dst) flag,
   * and an offset from UTC.
   */
  static class TZInfo {
    final String zoneId;
    final String abbr;
    final int dst;
    final int offset;

    public TZInfo(String zoneId, String abbr, int dst, int offset) {
      this.zoneId = zoneId;
      this.abbr = abbr;
      this.dst = dst;
      this.offset = offset;
    }

    static TZInfo decode(String zoneId, String raw) {
      String[] parts = raw.split(":");
      String abbr = parts[0];
      int dst = Integer.parseInt(parts[1]);
      int offset = Integer.parseInt(parts[2], 36) * 1000;
      return new TZInfo(zoneId, abbr, dst, offset);
    }

    @Override
    public String toString() {
      return "ZoneInfo(id=" + zoneId
          + " abbr=" + abbr
          + " dst=" + dst
          + " offset=" + offset
          + ")";
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy