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

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

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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import com.google.gson.JsonObject;
import com.squarespace.cldrengine.internal.LocaleExternalData;
import com.squarespace.cldrengine.locale.CLocaleImpl;
import com.squarespace.cldrengine.locale.DistanceTable;
import com.squarespace.cldrengine.locale.LanguageTagParser;
import com.squarespace.cldrengine.locale.LocaleMatch;
import com.squarespace.cldrengine.locale.LocaleResolver;
import com.squarespace.cldrengine.utils.JsonUtils;

import lombok.ToString;

public class LocaleMatcher {

  private static final Map PARADIGM_LOCALES = load();
  private static final Pattern TAG_SEP = Pattern.compile("[,\\s]+", Pattern.MULTILINE);

  private static final LanguageTag UNDEFINED = new LanguageTag();

  private final Map> exactMap = new HashMap<>();
  private final List supported;
  private Entry defaultEntry;
  private int count;

  public LocaleMatcher(String supportedLocales) {
    this.supported = parse(supportedLocales);
    init();
  }

  public LocaleMatcher(String[] supportedLocales) {
    this.supported = parse(supportedLocales);
    init();
  }

  public LocaleMatcher(List supportedLocales) {
    this.supported = parse(supportedLocales);
    init();
  }

  /**
   * Find the desired locale that is the closed match to a supported locale, within
   * the given threshold. Any matches whose distance is greater than or equal to the
   * threshold will be treated as having maximum distance.
   */
  public LocaleMatch match(String desiredLocales) {
    return match(desiredLocales, DistanceTable.DEFAULT_THRESHOLD);
  }

  /**
   * Find the desired locale that is the closed match to a supported locale, within
   * the given threshold. Any matches whose distance is greater than or equal to the
   * threshold will be treated as having maximum distance.
   */
  public LocaleMatch match(String desiredLocales, int threshold) {
    List desireds = parse(desiredLocales);
    return this._match(desireds, threshold);
  }

  /**
   * Find the desired locale that is the closed match to a supported locale, within
   * the given threshold. Any matches whose distance is greater than or equal to the
   * threshold will be treated as having maximum distance.
   */
  public LocaleMatch match(String[] desiredLocales) {
    return match(desiredLocales, DistanceTable.DEFAULT_THRESHOLD);
  }

  /**
   * Find the desired locale that is the closed match to a supported locale, within
   * the given threshold. Any matches whose distance is greater than or equal to the
   * threshold will be treated as having maximum distance.
   */
  public LocaleMatch match(String[] desiredLocales, int threshold) {
    List desireds = parse(desiredLocales);
    return this._match(desireds, threshold);
  }

  /**
   * Find the desired locale that is the closed match to a supported locale, within
   * the given threshold. Any matches whose distance is greater than or equal to the
   * threshold will be treated as having maximum distance.
   */
  public LocaleMatch match(List desiredLocales) {
    return match(desiredLocales, DistanceTable.DEFAULT_THRESHOLD);
  }

  /**
   * Find the desired locale that is the closed match to a supported locale, within
   * the given threshold. Any matches whose distance is greater than or equal to the
   * threshold will be treated as having maximum distance.
   */
  public LocaleMatch match(List desiredLocales, int threshold) {
    List desireds = parse(desiredLocales);
    return this._match(desireds, threshold);
  }

  private LocaleMatch _match(List desireds, int threshold) {
    int len = desireds.size();
    int bestDistance = DistanceTable.MAX_DISTANCE;
    Entry bestMatch = null;
    Entry bestDesired = len == 0 ? this.defaultEntry : desireds.get(0);
    for (int i = 0; i < len; i++) {
      Entry desired = desireds.get(i);
      List exact = this.exactMap.get(desired.compact);
      if (exact != null) {
        CLocale locale = new CLocaleImpl(exact.get(0).id, exact.get(0).tag);
        return new LocaleMatch(locale, 0);
      }

      for (int j = 0; j < this.count; j++) {
        Entry supported = this.supported.get(j);
        int distance = DistanceTable.distance(desired.tag, supported.tag, threshold);
        if (distance < bestDistance) {
          bestDistance = distance;
          bestMatch = supported;
          bestDesired = desired;
        }
      }
    }

    if (bestMatch == null) {
      bestMatch = this.defaultEntry;
    }

    LanguageTag tag = new LanguageTag(
        bestMatch.tag.language(),
        bestMatch.tag.script(),
        bestMatch.tag.region(),
        bestMatch.tag.variant(),
        bestDesired.tag.extensions(),
        bestDesired.tag.privateUse(),
        bestDesired.tag.extlangs()
    );
    CLocale locale = new CLocaleImpl(bestMatch.id, tag);
    return new LocaleMatch(locale, bestDistance);
  }

  private void init() {
    this.count = this.supported.size();

    // The first locale in the list is used as the default.
    this.defaultEntry = this.supported.get(0);

    this.supported.sort((Entry a, Entry b) -> {
      // Keep default tag at the front..
      if (a.tag == this.defaultEntry.tag) {
        return -1;
      }
      if (b.tag == this.defaultEntry.tag) {
        return 1;
      }

      // Sort all paradigm locales before non-paradigms.
      Integer pa = PARADIGM_LOCALES.get(a.compact);
      Integer pb = PARADIGM_LOCALES.get(b.compact);
      if (pa != null) {
        return pb == null ? -1 : Integer.compare(pa, pb);
      } else if (pb != null) {
        return 1;
      }

      // All other locales stay in their relative positions.
      return 0;
    });

    // Wire up a map for quick lookups of exact matches. These have a
    // distance of 0 and will short-circuit the matching loop.
    this.supported.forEach(locale -> {;
      String key = locale.compact;
      List bundles = this.exactMap.get(key);
      if (bundles == null) {
        bundles = new ArrayList<>();
        this.exactMap.put(key, bundles);
      }
      bundles.add(locale);
    });
  }

  private static List parse(String locales) {
    List res = new ArrayList<>();
    for (String locale : TAG_SEP.split(locales)) {
      res.add(parseEntry(locale));
    }
    return res;
  }

  private static List parse(List locales) {
    List res = new ArrayList<>();
    for (String locale : locales) {
      res.addAll(parse(locale));
    }
    return res;
  }

  private static List parse(String[] locales) {
    List res = new ArrayList<>();
    for (String locale : locales) {
      res.addAll(parse(locale));
    }
    return res;
  }

  private static Entry parseEntry(String raw) {
    String id = raw.trim();
    LanguageTag tag = LanguageTagParser.parse(id);
    if (tag.hasLanguage() || tag.hasScript() || tag.hasRegion()) {
      return new Entry(id, LocaleResolver.resolve(tag));
    }
    return new Entry(id, UNDEFINED);
  }

  @ToString
  static class Entry {

    private final String id;
    private final LanguageTag tag;
    private final String compact;

    public Entry(String id, LanguageTag tag) {
      this.id = id;
      this.tag = tag;
      this.compact = tag.compact();
    }
  }

  private static final Map load() {
    JsonObject elems = JsonUtils.parse(LocaleExternalData.PARADIGMLOCALES).getAsJsonObject();
    Map res = new HashMap<>();
    for (String raw : elems.keySet()) {
      int i = elems.get(raw).getAsInt();
      String compact = LocaleResolver.resolve(raw).compact();
      res.put(compact, i);
    }
    return res;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy