blob: a64137d660f5e2e667f292bfc4c3d88be110e91c [file] [log] [blame]
package org.unicode.cldr.util;
import com.google.common.base.Splitter;
import com.ibm.icu.dev.util.UnicodeMap;
import com.ibm.icu.impl.Relation;
import com.ibm.icu.impl.Row;
import com.ibm.icu.lang.UCharacter;
import com.ibm.icu.text.Collator;
import com.ibm.icu.text.Transform;
import com.ibm.icu.text.UnicodeSet;
import com.ibm.icu.util.ICUException;
import com.ibm.icu.util.Output;
import com.ibm.icu.util.ULocale;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.unicode.cldr.draft.ScriptMetadata;
import org.unicode.cldr.draft.ScriptMetadata.Info;
import org.unicode.cldr.tool.LikelySubtags;
import org.unicode.cldr.util.RegexLookup.Finder;
import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo.Count;
import org.unicode.cldr.util.With.SimpleIterator;
import org.unicode.cldr.util.personname.PersonNameFormatter;
/**
* Provides a mechanism for dividing up LDML paths into understandable categories, eg for the Survey
* tool.
*/
public class PathHeader implements Comparable<PathHeader> {
/** Link to a section. Commenting out the page switch for now. */
public static final String SECTION_LINK = "<a " + /* "target='CLDR_ST-SECTION' "+*/ "href='";
static boolean UNIFORM_CONTINENTS = true;
static Factory factorySingleton = null;
static final boolean SKIP_ORIGINAL_PATH = true;
private static final Logger logger = Logger.getLogger(PathHeader.class.getName());
static final Splitter HYPHEN_SPLITTER = Splitter.on('-');
public enum Width {
FULL,
LONG,
WIDE,
SHORT,
NARROW;
public static Width getValue(String input) {
try {
return Width.valueOf(input.toUpperCase(Locale.ENGLISH));
} catch (RuntimeException e) {
e.printStackTrace();
throw e;
}
}
@Override
public String toString() {
return name().toLowerCase(Locale.ENGLISH);
}
}
/** What status the survey tool should use. Can be overridden in Phase.getAction() */
public enum SurveyToolStatus {
/** Never show. */
DEPRECATED,
/** Hide. Can be overridden in Phase.getAction() */
HIDE,
/**
* Don't allow Change box (except TC), instead show ticket. But allow votes. Can be
* overridden in Phase.getAction()
*/
READ_ONLY,
/** Allow change box and votes. Can be overridden in Phase.getAction() */
READ_WRITE,
/**
* Changes are allowed as READ_WRITE, but field is always displayed as LTR, even in RTL
* locales (used for patterns).
*/
LTR_ALWAYS
}
private static final EnumNames<SectionId> SectionIdNames = new EnumNames<>();
/**
* The Section for a path. Don't change these without committee buy-in. The 'name' may be
* 'Core_Data' and the toString is 'Core Data' toString gives the human name
*/
public enum SectionId {
Core_Data("Core Data"),
Locale_Display_Names("Locale Display Names"),
DateTime("Date & Time"),
Timezones,
Numbers,
Currencies,
Units,
Characters,
Misc("Miscellaneous"),
BCP47,
Supplemental,
Special;
SectionId(String... alternateNames) {
SectionIdNames.add(this, alternateNames);
}
public static SectionId forString(String name) {
return SectionIdNames.forString(name);
}
@Override
public String toString() {
return SectionIdNames.toString(this);
}
}
private static final EnumNames<PageId> PageIdNames = new EnumNames<>();
private static final Relation<SectionId, PageId> SectionIdToPageIds =
Relation.of(new TreeMap<>(), TreeSet.class);
private static class SubstringOrder implements Comparable<SubstringOrder> {
final String mainOrder;
final int order;
public SubstringOrder(String source) {
int pos = source.lastIndexOf('-') + 1;
int ordering = COUNTS.indexOf(source.substring(pos));
// account for digits, and "some" future proofing.
order = ordering < 0 ? source.charAt(pos) : 0x10000 + ordering;
mainOrder = source.substring(0, pos);
}
@Override
public String toString() {
return "{" + mainOrder + ", " + order + "}";
}
@Override
public int compareTo(SubstringOrder other) {
int diff = alphabeticCompare(mainOrder, other.mainOrder);
if (diff != 0) {
return diff;
}
return order - other.order;
}
}
/**
* The Page for a path (within a Section). Don't change these without committee buy-in. the name
* is for example WAsia where toString gives Western Asia
*/
public enum PageId {
Alphabetic_Information(SectionId.Core_Data, "Alphabetic Information"),
Numbering_Systems(SectionId.Core_Data, "Numbering Systems"),
LinguisticElements(SectionId.Core_Data, "Linguistic Elements"),
Locale_Name_Patterns(SectionId.Locale_Display_Names, "Locale Name Patterns"),
Languages_A_D(SectionId.Locale_Display_Names, "Languages (A-D)"),
Languages_E_J(SectionId.Locale_Display_Names, "Languages (E-J)"),
Languages_K_N(SectionId.Locale_Display_Names, "Languages (K-N)"),
Languages_O_S(SectionId.Locale_Display_Names, "Languages (O-S)"),
Languages_T_Z(SectionId.Locale_Display_Names, "Languages (T-Z)"),
Scripts(SectionId.Locale_Display_Names),
Territories(SectionId.Locale_Display_Names, "Geographic Regions"),
T_NAmerica(SectionId.Locale_Display_Names, "Territories (North America)"),
T_SAmerica(SectionId.Locale_Display_Names, "Territories (South America)"),
T_Africa(SectionId.Locale_Display_Names, "Territories (Africa)"),
T_Europe(SectionId.Locale_Display_Names, "Territories (Europe)"),
T_Asia(SectionId.Locale_Display_Names, "Territories (Asia)"),
T_Oceania(SectionId.Locale_Display_Names, "Territories (Oceania)"),
Locale_Variants(SectionId.Locale_Display_Names, "Locale Variants"),
Keys(SectionId.Locale_Display_Names),
Fields(SectionId.DateTime),
Gregorian(SectionId.DateTime),
Generic(SectionId.DateTime),
Buddhist(SectionId.DateTime),
Chinese(SectionId.DateTime),
Coptic(SectionId.DateTime),
Dangi(SectionId.DateTime),
Ethiopic(SectionId.DateTime),
Ethiopic_Amete_Alem(SectionId.DateTime, "Ethiopic-Amete-Alem"),
Hebrew(SectionId.DateTime),
Indian(SectionId.DateTime),
Islamic(SectionId.DateTime),
Japanese(SectionId.DateTime),
Persian(SectionId.DateTime),
Minguo(SectionId.DateTime),
Timezone_Display_Patterns(SectionId.Timezones, "Timezone Display Patterns"),
NAmerica(SectionId.Timezones, "North America"),
SAmerica(SectionId.Timezones, "South America"),
Africa(SectionId.Timezones),
Europe(SectionId.Timezones),
Russia(SectionId.Timezones),
WAsia(SectionId.Timezones, "Western Asia"),
CAsia(SectionId.Timezones, "Central Asia"),
EAsia(SectionId.Timezones, "Eastern Asia"),
SAsia(SectionId.Timezones, "Southern Asia"),
SEAsia(SectionId.Timezones, "Southeast Asia"),
Australasia(SectionId.Timezones),
Antarctica(SectionId.Timezones),
Oceania(SectionId.Timezones),
UnknownT(SectionId.Timezones, "Unknown Region"),
Overrides(SectionId.Timezones),
Symbols(SectionId.Numbers),
Number_Formatting_Patterns(SectionId.Numbers, "Number Formatting Patterns"),
Compact_Decimal_Formatting(SectionId.Numbers, "Compact Decimal Formatting"),
Compact_Decimal_Formatting_Other(
SectionId.Numbers, "Compact Decimal Formatting (Other Numbering Systems)"),
Measurement_Systems(SectionId.Units, "Measurement Systems"),
Duration(SectionId.Units),
Graphics(SectionId.Units),
Length(SectionId.Units),
Area(SectionId.Units),
Volume_Metric(SectionId.Units, "Volume Metric"),
Volume_Other(SectionId.Units, "Volume Other"),
SpeedAcceleration(SectionId.Units, "Speed and Acceleration"),
MassWeight(SectionId.Units, "Mass and Weight"),
EnergyPower(SectionId.Units, "Energy and Power"),
ElectricalFrequency(SectionId.Units, "Electrical and Frequency"),
Weather(SectionId.Units),
Digital(SectionId.Units),
Coordinates(SectionId.Units),
OtherUnits(SectionId.Units, "Other Units"),
CompoundUnits(SectionId.Units, "Compound Units"),
Displaying_Lists(SectionId.Misc, "Displaying Lists"),
MinimalPairs(SectionId.Misc, "Minimal Pairs"),
PersonNameFormats(SectionId.Misc, "Person Name Formats"),
Transforms(SectionId.Misc),
Identity(SectionId.Special),
Version(SectionId.Special),
Suppress(SectionId.Special),
Deprecated(SectionId.Special),
Unknown(SectionId.Special),
C_NAmerica(SectionId.Currencies, "North America (C)"),
// need to add (C) to differentiate from Timezone territories
C_SAmerica(SectionId.Currencies, "South America (C)"),
C_NWEurope(SectionId.Currencies, "Northern/Western Europe"),
C_SEEurope(SectionId.Currencies, "Southern/Eastern Europe"),
C_NAfrica(SectionId.Currencies, "Northern Africa"),
C_WAfrica(SectionId.Currencies, "Western Africa"),
C_MAfrica(SectionId.Currencies, "Middle Africa"),
C_EAfrica(SectionId.Currencies, "Eastern Africa"),
C_SAfrica(SectionId.Currencies, "Southern Africa"),
C_WAsia(SectionId.Currencies, "Western Asia (C)"),
C_CAsia(SectionId.Currencies, "Central Asia (C)"),
C_EAsia(SectionId.Currencies, "Eastern Asia (C)"),
C_SAsia(SectionId.Currencies, "Southern Asia (C)"),
C_SEAsia(SectionId.Currencies, "Southeast Asia (C)"),
C_Oceania(SectionId.Currencies, "Oceania (C)"),
C_Unknown(SectionId.Currencies, "Unknown Region (C)"),
// BCP47
u_Extension(SectionId.BCP47),
t_Extension(SectionId.BCP47),
// Supplemental
Alias(SectionId.Supplemental),
IdValidity(SectionId.Supplemental),
Locale(SectionId.Supplemental),
RegionMapping(SectionId.Supplemental),
WZoneMapping(SectionId.Supplemental),
Transform(SectionId.Supplemental),
Units(SectionId.Supplemental),
Likely(SectionId.Supplemental),
LanguageMatch(SectionId.Supplemental),
TerritoryInfo(SectionId.Supplemental),
LanguageInfo(SectionId.Supplemental),
LanguageGroup(SectionId.Supplemental),
Fallback(SectionId.Supplemental),
Gender(SectionId.Supplemental),
Grammar(SectionId.Supplemental),
Metazone(SectionId.Supplemental),
NumberSystem(SectionId.Supplemental),
Plural(SectionId.Supplemental),
PluralRange(SectionId.Supplemental),
Containment(SectionId.Supplemental),
Currency(SectionId.Supplemental),
Calendar(SectionId.Supplemental),
WeekData(SectionId.Supplemental),
Measurement(SectionId.Supplemental),
Language(SectionId.Supplemental),
RBNF(SectionId.Supplemental),
Segmentation(SectionId.Supplemental),
DayPeriod(SectionId.Supplemental),
Category(SectionId.Characters),
// [Smileys, People, Animals & Nature, Food & Drink, Travel & Places, Activities, Objects,
// Symbols, Flags]
Smileys(SectionId.Characters, "Smileys & Emotion"),
People(SectionId.Characters, "People & Body"),
People2(SectionId.Characters, "People & Body 2"),
Animals_Nature(SectionId.Characters, "Animals & Nature"),
Food_Drink(SectionId.Characters, "Food & Drink"),
Travel_Places(SectionId.Characters, "Travel & Places"),
Travel_Places2(SectionId.Characters, "Travel & Places 2"),
Activities(SectionId.Characters),
Objects(SectionId.Characters),
Objects2(SectionId.Characters),
EmojiSymbols(SectionId.Characters, "Emoji Symbols"),
Punctuation(SectionId.Characters),
MathSymbols(SectionId.Characters, "Math Symbols"),
OtherSymbols(SectionId.Characters, "Other Symbols"),
Flags(SectionId.Characters),
Component(SectionId.Characters),
Typography(SectionId.Characters),
;
private final SectionId sectionId;
PageId(SectionId sectionId, String... alternateNames) {
this.sectionId = sectionId;
SectionIdToPageIds.put(sectionId, this);
PageIdNames.add(this, alternateNames);
}
/**
* Construct a pageId given a string
*
* @param name
* @return
*/
public static PageId forString(String name) {
try {
return PageIdNames.forString(name);
} catch (Exception e) {
throw new ICUException("No PageId for " + name, e);
}
}
/**
* Returns the page id
*
* @return a page ID, such as 'Languages'
*/
@Override
public String toString() {
return PageIdNames.toString(this);
}
/**
* Get the containing section id, such as 'Code Lists'
*
* @return the containing section ID
*/
public SectionId getSectionId() {
return sectionId;
}
}
private final SectionId sectionId;
private final PageId pageId;
private final String header;
private final String code;
private final String originalPath;
private final SurveyToolStatus status;
// Used for ordering
private final int headerOrder;
private final long codeOrder;
private final SubstringOrder codeSuborder;
static final Pattern SEMI = PatternCache.get("\\s*;\\s*");
static final Matcher ALT_MATCHER = PatternCache.get("\\[@alt=\"([^\"]*+)\"]").matcher("");
static final SupplementalDataInfo supplementalDataInfo = SupplementalDataInfo.getInstance();
static final Map<String, String> metazoneToContinent =
supplementalDataInfo.getMetazoneToContinentMap();
static final Map<String, String> metazoneToPageTerritory = new HashMap<>();
static {
Map<String, Map<String, String>> metazoneToRegionToZone =
supplementalDataInfo.getMetazoneToRegionToZone();
for (Entry<String, Map<String, String>> metazoneEntry : metazoneToRegionToZone.entrySet()) {
String metazone = metazoneEntry.getKey();
String worldZone = metazoneEntry.getValue().get("001");
String territory = Containment.getRegionFromZone(worldZone);
if (territory == null) {
territory = "ZZ";
}
// Russia, Antarctica => territory
// in Australasia, Asia, S. America => subcontinent
// in N. America => N. America (grouping of 3 subcontinents)
// in everything else => continent
if (territory.equals("RU") || territory.equals("AQ")) {
metazoneToPageTerritory.put(metazone, territory);
} else {
String continent = Containment.getContinent(territory);
String subcontinent = Containment.getSubcontinent(territory);
if (continent.equals("142")) { // Asia
metazoneToPageTerritory.put(metazone, subcontinent);
} else if (continent.equals("019")) { // Americas
metazoneToPageTerritory.put(
metazone, subcontinent.equals("005") ? subcontinent : "003");
} else if (subcontinent.equals("053")) { // Australasia
metazoneToPageTerritory.put(metazone, subcontinent);
} else {
metazoneToPageTerritory.put(metazone, continent);
}
}
}
}
private PathHeader(
SectionId sectionId,
PageId pageId,
String header,
int headerOrder,
String code,
long codeOrder,
SubstringOrder suborder,
SurveyToolStatus status,
String originalPath) {
this.sectionId = sectionId;
this.pageId = pageId;
this.header = header;
this.headerOrder = headerOrder;
this.code = code;
this.codeOrder = codeOrder;
this.codeSuborder = suborder;
this.originalPath = originalPath;
this.status = status;
}
/**
* Return a factory for use in creating the headers. This is cached after first use. The calls
* are thread-safe. Null gets the default (CLDRConfig) english file.
*
* @param englishFile
*/
public static Factory getFactory(CLDRFile englishFile) {
if (factorySingleton == null) {
if (englishFile == null) {
englishFile = CLDRConfig.getInstance().getEnglish();
}
if (!englishFile.getLocaleID().equals(ULocale.ENGLISH.getBaseName())) {
throw new IllegalArgumentException(
"PathHeader's CLDRFile must be '"
+ ULocale.ENGLISH.getBaseName()
+ "', but found '"
+ englishFile.getLocaleID()
+ "'");
}
factorySingleton = new Factory(englishFile);
}
return factorySingleton;
}
/** Convenience method for common case. See {{@link #getFactory(CLDRFile)}} */
public static Factory getFactory() {
return getFactory(null);
}
/**
* @deprecated
*/
@Deprecated
public String getSection() {
return sectionId.toString();
}
public SectionId getSectionId() {
return sectionId;
}
/**
* @deprecated
*/
@Deprecated
public String getPage() {
return pageId.toString();
}
public PageId getPageId() {
return pageId;
}
public String getHeader() {
return header == null ? "" : header;
}
public String getCode() {
return code;
}
public String getHeaderCode() {
return getHeader() + ": " + getCode();
}
public String getOriginalPath() {
return originalPath;
}
public SurveyToolStatus getSurveyToolStatus() {
return status;
}
@Override
public String toString() {
return sectionId
+ "\t"
+ pageId
+ "\t"
+ header // + "\t" + headerOrder
+ "\t"
+ code // + "\t" + codeOrder
;
}
/**
* Compare this PathHeader to another one
*
* @param other the object to be compared.
* @return 0 if equal, -1 if less, 1 if more
* <p>Note: if we ever have to compare just the header or just the code, methods to do that
* were in release 44 (compareHeader and compareCode), but they were unused and therefore
* removed in CLDR-11155.
*/
@Override
public int compareTo(PathHeader other) {
// Within each section, order alphabetically if the integer orders are
// not different.
try {
int result;
if (0 != (result = sectionId.compareTo(other.sectionId))) {
return result;
}
if (0 != (result = pageId.compareTo(other.pageId))) {
return result;
}
if (0 != (result = headerOrder - other.headerOrder)) {
return result;
}
if (0 != (result = alphabeticCompare(header, other.header))) {
return result;
}
long longResult;
if (0 != (longResult = codeOrder - other.codeOrder)) {
return longResult < 0 ? -1 : 1;
}
if (codeSuborder != null) { // do all three cases, for transitivity
if (other.codeSuborder != null) {
if (0 != (result = codeSuborder.compareTo(other.codeSuborder))) {
return result;
}
} else {
return 1; // if codeSuborder != null (and other.codeSuborder
// == null), it is greater
}
} else if (other.codeSuborder != null) {
return -1; // if codeSuborder == null (and other.codeSuborder !=
// null), it is greater
}
if (0 != (result = alphabeticCompare(code, other.code))) {
return result;
}
if (!SKIP_ORIGINAL_PATH) {
if (0 != (result = alphabeticCompare(originalPath, other.originalPath))) {
return result;
}
}
return 0;
} catch (RuntimeException e) {
throw new IllegalArgumentException(
"Internal problem comparing " + this + " and " + other, e);
}
}
@Override
public boolean equals(Object obj) {
PathHeader other;
try {
other = (PathHeader) obj;
} catch (Exception e) {
return false;
}
return sectionId == other.sectionId
&& pageId == other.pageId
&& header.equals(other.header)
&& code.equals(other.code);
}
@Override
public int hashCode() {
return sectionId.hashCode() ^ pageId.hashCode() ^ header.hashCode() ^ code.hashCode();
}
public static class Factory implements Transform<String, PathHeader> {
static final RegexLookup<RawData> lookup =
RegexLookup.of(new PathHeaderTransform())
.setPatternTransform(RegexLookup.RegexFinderTransformPath)
.loadFromFile(PathHeader.class, "data/PathHeader.txt");
// synchronized with lookup
static final Output<String[]> args = new Output<>();
// synchronized with lookup
static final Counter<RawData> counter = new Counter<>();
// synchronized with lookup
static final Map<RawData, String> samples = new HashMap<>();
// synchronized with lookup
static long order;
static SubstringOrder suborder;
static final Map<String, PathHeader> cache = new HashMap<>();
// synchronized with cache
static final Map<SectionId, Map<PageId, SectionPage>> sectionToPageToSectionPage =
new EnumMap<>(SectionId.class);
static final Relation<SectionPage, String> sectionPageToPaths =
Relation.of(new TreeMap<>(), HashSet.class);
private static CLDRFile englishFile;
private final Set<String> matchersFound = new HashSet<>();
/**
* Create a factory for creating PathHeaders.
*
* @param englishFile - only sets the file (statically!) if not already set.
*/
private Factory(CLDRFile englishFile) {
setEnglishCLDRFileIfNotSet(englishFile); // temporary
}
/**
* Set englishFile if it is not already set.
*
* @param englishFile2 the value to set for englishFile
*/
private static void setEnglishCLDRFileIfNotSet(CLDRFile englishFile2) {
synchronized (Factory.class) {
if (englishFile == null) {
englishFile = englishFile2;
}
}
}
/** Use only when trying to find unmatched patterns */
public void clearCache() {
synchronized (cache) {
cache.clear();
}
}
/** Return the PathHeader for a given path. Thread-safe. */
public PathHeader fromPath(String path) {
return fromPath(path, null);
}
/** Return the PathHeader for a given path. Thread-safe. */
@Override
public PathHeader transform(String path) {
return fromPath(path, null);
}
/**
* Return the PathHeader for a given path. Thread-safe.
*
* @param failures a list of failures to add to.
*/
public PathHeader fromPath(final String path, List<String> failures) {
if (path == null) {
throw new NullPointerException("Path cannot be null");
}
synchronized (cache) {
PathHeader old = cache.get(path);
if (old != null) {
return old;
}
}
synchronized (lookup) {
String cleanPath = path;
// special handling for alt
String alt = null;
int altPos = cleanPath.indexOf("[@alt=");
if (altPos >= 0 && !cleanPath.endsWith("/symbol[@alt=\"narrow\"]")) {
if (ALT_MATCHER.reset(cleanPath).find()) {
alt = ALT_MATCHER.group(1);
cleanPath =
cleanPath.substring(0, ALT_MATCHER.start())
+ cleanPath.substring(ALT_MATCHER.end());
int pos = alt.indexOf("proposed");
if (pos >= 0 && !path.startsWith("//ldml/collations")) {
alt = pos == 0 ? null : alt.substring(0, pos - 1);
// drop "proposed",
// change "xxx-proposed" to xxx.
}
} else {
throw new IllegalArgumentException();
}
}
Output<Finder> matcherFound = new Output<>();
RawData data = lookup.get(cleanPath, null, args, matcherFound, failures);
if (data == null) {
return null;
}
matchersFound.add(matcherFound.value.toString());
counter.add(data, 1);
if (!samples.containsKey(data)) {
samples.put(data, cleanPath);
}
try {
PathHeader result = makePathHeader(data, path, alt);
synchronized (cache) {
PathHeader old = cache.get(path);
if (old == null) {
cache.put(path, result);
} else {
result = old;
}
Map<PageId, SectionPage> pageToPathHeaders =
sectionToPageToSectionPage.get(result.sectionId);
if (pageToPathHeaders == null) {
sectionToPageToSectionPage.put(
result.sectionId,
pageToPathHeaders = new EnumMap<>(PageId.class));
}
SectionPage sectionPage = pageToPathHeaders.get(result.pageId);
if (sectionPage == null) {
sectionPage = new SectionPage(result.sectionId, result.pageId);
pageToPathHeaders.put(result.pageId, sectionPage);
}
sectionPageToPaths.put(sectionPage, path);
}
return result;
} catch (Exception e) {
throw new IllegalArgumentException(
"Probably mismatch in Page/Section enum, or too few capturing groups in regex for "
+ path,
e);
}
}
}
private PathHeader makePathHeader(RawData data, String path, String alt) {
// Caution: each call to PathHeader.Factory.fix changes the value of
// PathHeader.Factory.order
SectionId newSectionId = SectionId.forString(fix(data.section, 0));
PageId newPageId = PageId.forString(fix(data.page, 0));
String newHeader = fix(data.header, data.headerOrder);
int newHeaderOrder = (int) order;
String codeDashAlt = data.code + (alt == null ? "" : ("-" + alt));
String newCode = fix(codeDashAlt, data.codeOrder);
long newCodeOrder = order;
return new PathHeader(
newSectionId,
newPageId,
newHeader,
newHeaderOrder,
newCode,
newCodeOrder,
suborder,
data.status,
path);
}
private static class SectionPage implements Comparable<SectionPage> {
private final SectionId sectionId;
private final PageId pageId;
public SectionPage(SectionId sectionId, PageId pageId) {
this.sectionId = sectionId;
this.pageId = pageId;
}
@Override
public int compareTo(SectionPage other) {
// Within each section, order alphabetically if the integer
// orders are
// not different.
int result;
if (0 != (result = sectionId.compareTo(other.sectionId))) {
return result;
}
if (0 != (result = pageId.compareTo(other.pageId))) {
return result;
}
return 0;
}
@Override
public boolean equals(Object obj) {
PathHeader other;
try {
other = (PathHeader) obj;
} catch (Exception e) {
return false;
}
return sectionId == other.sectionId && pageId == other.pageId;
}
@Override
public int hashCode() {
return sectionId.hashCode() ^ pageId.hashCode();
}
@Override
public String toString() {
return sectionId + " > " + pageId;
}
}
/**
* Returns a set of paths currently associated with the given section and page.
*
* <p><b>Warning:</b>
*
* <ol>
* <li>The set may not be complete for a cldrFile unless all of paths in the file have had
* fromPath called. And this includes getExtraPaths().
* <li>The set may include paths that have no value in the current cldrFile.
* <li>The set may be empty, if the section/page aren't valid.
* </ol>
*
* Thread-safe.
*/
public static Set<String> getCachedPaths(SectionId sectionId, PageId page) {
Set<String> target = new HashSet<>();
synchronized (cache) {
Map<PageId, SectionPage> pageToSectionPage =
sectionToPageToSectionPage.get(sectionId);
if (pageToSectionPage == null) {
return target;
}
SectionPage sectionPage = pageToSectionPage.get(page);
if (sectionPage == null) {
return target;
}
Set<String> set = sectionPageToPaths.getAll(sectionPage);
target.addAll(set);
}
return target;
}
/**
* Return the Sections and Pages that are in defined, for display in menus. Both are
* ordered.
*/
public static Relation<SectionId, PageId> getSectionIdsToPageIds() {
SectionIdToPageIds.freeze(); // just in case
return SectionIdToPageIds;
}
/**
* Return the names for Sections and Pages that are defined, for display in menus. Both are
* ordered.
*
* @deprecated Use getSectionIdsToPageIds
*/
@Deprecated
public static LinkedHashMap<String, Set<String>> getSectionsToPages() {
LinkedHashMap<String, Set<String>> sectionsToPages = new LinkedHashMap<>();
for (PageId pageId : PageId.values()) {
String sectionId2 = pageId.getSectionId().toString();
Set<String> pages =
sectionsToPages.computeIfAbsent(sectionId2, k -> new LinkedHashSet<>());
pages.add(pageId.toString());
}
return sectionsToPages;
}
/**
* @deprecated, use the filterCldr with the section/page ids.
*/
public Iterable<String> filterCldr(String section, String page, CLDRFile file) {
return new FilteredIterable(section, page, file);
}
private class FilteredIterable implements Iterable<String>, SimpleIterator<String> {
private final SectionId sectionId;
private final PageId pageId;
private final Iterator<String> fileIterator;
FilteredIterable(SectionId sectionId, PageId pageId, CLDRFile file) {
this.sectionId = sectionId;
this.pageId = pageId;
this.fileIterator = file.fullIterable().iterator();
}
public FilteredIterable(String section, String page, CLDRFile file) {
this(SectionId.forString(section), PageId.forString(page), file);
}
@Override
public Iterator<String> iterator() {
return With.toIterator(this);
}
@Override
public String next() {
while (fileIterator.hasNext()) {
String path = fileIterator.next();
PathHeader pathHeader = fromPath(path);
if (sectionId == pathHeader.sectionId && pageId == pathHeader.pageId) {
return path;
}
}
return null;
}
}
private static class ChronologicalOrder {
private final Map<String, Integer> map = new HashMap<>();
private String item;
private int order;
private final ChronologicalOrder toClear;
ChronologicalOrder(ChronologicalOrder toClear) {
this.toClear = toClear;
}
int getOrder() {
return order;
}
public String set(String itemToOrder) {
if (itemToOrder.startsWith("*")) {
item = itemToOrder.substring(1, itemToOrder.length());
return item; // keep old order
}
item = itemToOrder;
Integer old = map.get(item);
if (old != null) {
order = old.intValue();
} else {
order = map.size();
map.put(item, order);
clearLower();
}
return item;
}
private void clearLower() {
if (toClear != null) {
toClear.map.clear();
toClear.order = 0;
toClear.clearLower();
}
}
}
static class RawData {
static ChronologicalOrder codeOrdering = new ChronologicalOrder(null);
static ChronologicalOrder headerOrdering = new ChronologicalOrder(codeOrdering);
public RawData(String source) {
String[] split = SEMI.split(source);
section = split[0];
// HACK
if (section.equals("Timezones") && split[1].equals("Indian")) {
page = "Indian2";
} else {
page = split[1];
}
header = headerOrdering.set(split[2]);
headerOrder = headerOrdering.getOrder();
code = codeOrdering.set(split[3]);
codeOrder = codeOrdering.getOrder();
status =
split.length < 5
? SurveyToolStatus.READ_WRITE
: SurveyToolStatus.valueOf(split[4]);
}
public final String section;
public final String page;
public final String header;
public final int headerOrder;
public final String code;
public final int codeOrder;
public final SurveyToolStatus status;
@Override
public String toString() {
return section
+ "\t"
+ page
+ "\t"
+ header
+ "\t"
+ headerOrder
+ "\t"
+ code
+ "\t"
+ codeOrder
+ "\t"
+ status;
}
}
static class PathHeaderTransform implements Transform<String, RawData> {
@Override
public RawData transform(String source) {
return new RawData(source);
}
}
/**
* Internal data, for testing and debugging.
*
* @deprecated
*/
@Deprecated
public class CounterData extends Row.R4<String, RawData, String, String> {
public CounterData(String a, RawData b, String c) {
super(
a,
b,
c == null ? "no sample" : c,
c == null ? "no sample" : fromPath(c).toString());
}
}
/**
* Get the internal data, for testing and debugging.
*
* @deprecated
*/
@Deprecated
public Counter<CounterData> getInternalCounter() {
synchronized (lookup) {
Counter<CounterData> result = new Counter<>();
for (Map.Entry<Finder, RawData> foo : lookup) {
Finder finder = foo.getKey();
RawData data = foo.getValue();
long count = counter.get(data);
result.add(new CounterData(finder.toString(), data, samples.get(data)), count);
}
return result;
}
}
static Map<String, Transform<String, String>> functionMap = new HashMap<>();
static String[] months = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
"Und"
};
static List<String> days = Arrays.asList("sun", "mon", "tue", "wed", "thu", "fri", "sat");
static List<String> unitOrder = DtdData.getUnitOrder().getOrder();
static final MapComparator<String> dayPeriods =
new MapComparator<String>()
.add(
"am",
"pm",
"midnight",
"noon",
"morning1",
"morning2",
"afternoon1",
"afternoon2",
"evening1",
"evening2",
"night1",
"night2")
.freeze();
static LikelySubtags likelySubtags = new LikelySubtags();
static HyphenSplitter hyphenSplitter = new HyphenSplitter();
static Transform<String, String> catFromTerritory;
static Transform<String, String> catFromTimezone;
static {
// Put any new functions used in PathHeader.txt in here.
// To change the order of items within a section or heading, set
// order/suborder to be the relative position of the current item.
functionMap.put(
"month",
new Transform<>() {
@Override
public String transform(String source) {
int m = Integer.parseInt(source);
order = m;
return months[m - 1];
}
});
functionMap.put(
"count",
new Transform<>() {
@Override
public String transform(String source) {
suborder = new SubstringOrder(source);
return source;
}
});
functionMap.put(
"count2",
new Transform<>() {
@Override
public String transform(String source) {
int pos = source.indexOf('-');
source = pos + source.substring(pos);
suborder = new SubstringOrder(source); // make 10000-...
// into 5-
return source;
}
});
functionMap.put(
"currencySymbol",
new Transform<>() {
@Override
public String transform(String source) {
order = 901;
if (source.endsWith("narrow")) {
order = 902;
}
if (source.endsWith("variant")) {
order = 903;
}
return source;
}
});
// &unitCount($1-$3-$4), where $1 is length, $2 is count, $3 is case (optional)
// but also
// &unitCount($1-$3-$5-$4), where $5 is case, $4 is gender — notice order change
functionMap.put(
"unitCount",
new Transform<>() {
@Override
public String transform(String source) {
List<String> parts = HYPHEN_SPLITTER.splitToList(source);
if (parts.size() == 1) {
return source;
}
int lengthNumber = Width.getValue(parts.get(0)).ordinal();
int type = 0;
int rest = 0;
switch (parts.get(1)) {
case "gender":
type = 0;
break;
case "displayName":
type = 1;
break;
case "per":
type = 2;
break;
default:
type = 3;
int countNumber =
(parts.size() > 1
? Count.valueOf(parts.get(1))
: Count.other)
.ordinal();
int caseNumber =
(parts.size() > 2
? GrammarInfo.CaseValues.valueOf(
parts.get(2))
: GrammarInfo.CaseValues.nominative)
.ordinal();
int genderNumber = GrammarInfo.GenderValues.neuter.ordinal();
if (parts.size() > 3) {
String genderPart = parts.get(3);
if (!genderPart.equals("dgender")) {
genderNumber =
GrammarInfo.GenderValues.valueOf(genderPart)
.ordinal();
}
type = 4;
}
rest = (countNumber << 16) | (caseNumber << 8) | genderNumber;
break;
}
order = (type << 28) | (lengthNumber << 24) | rest;
return source;
}
});
functionMap.put(
"pluralNumber",
new Transform<>() {
@Override
public String transform(String source) {
order = GrammarInfo.PluralValues.valueOf(source).ordinal();
return source;
}
});
functionMap.put(
"caseNumber",
new Transform<>() {
@Override
public String transform(String source) {
order = GrammarInfo.CaseValues.valueOf(source).ordinal();
return source;
}
});
functionMap.put(
"genderNumber",
new Transform<>() {
@Override
public String transform(String source) {
order = GrammarInfo.GenderValues.valueOf(source).ordinal();
return source;
}
});
functionMap.put(
"day",
new Transform<>() {
@Override
public String transform(String source) {
int m = days.indexOf(source);
order = m;
return source;
}
});
functionMap.put(
"dayPeriod",
new Transform<>() {
@Override
public String transform(String source) {
try {
order = dayPeriods.getNumericOrder(source);
} catch (Exception e) {
// if an old item is tried, like "evening", this will fail.
// so that old data still works, hack this.
order = Math.abs(source.hashCode() << 16);
}
return source;
}
});
functionMap.put(
"calendar",
new Transform<>() {
final Map<String, String> fixNames =
Builder.with(new HashMap<String, String>())
.put("islamicc", "Islamic Civil")
.put("roc", "Minguo")
.put("Ethioaa", "Ethiopic Amete Alem")
.put("Gregory", "Gregorian")
.put("iso8601", "ISO 8601")
.freeze();
@Override
public String transform(String source) {
String result = fixNames.get(source);
return result != null ? result : UCharacter.toTitleCase(source, null);
}
});
functionMap.put(
"calField",
new Transform<>() {
@Override
public String transform(String source) {
String[] fields = source.split(":", 3);
order = 0;
final List<String> widthValues =
Arrays.asList("wide", "abbreviated", "short", "narrow");
final List<String> calendarFieldValues =
Arrays.asList(
"Eras",
"Quarters",
"Months",
"Days",
"DayPeriods",
"Formats");
final List<String> calendarFormatTypes =
Arrays.asList("Standard", "Flexible", "Intervals");
final List<String> calendarContextTypes =
Arrays.asList("none", "format", "stand-alone");
final List<String> calendarFormatSubtypes =
Arrays.asList(
"date",
"time",
"time12",
"time24",
"dateTime",
"fallback");
Map<String, String> fixNames =
Builder.with(new HashMap<String, String>())
.put("DayPeriods", "Day Periods")
.put("format", "Formatting")
.put("stand-alone", "Standalone")
.put("none", "")
.put("date", "Date Formats")
.put("time", "Time Formats")
.put("time12", "12 Hour Time Formats")
.put("time24", "24 Hour Time Formats")
.put("dateTime", "Date & Time Combination Formats")
.freeze();
if (calendarFieldValues.contains(fields[0])) {
order = calendarFieldValues.indexOf(fields[0]) * 100;
} else {
order = calendarFieldValues.size() * 100;
}
if (fields[0].equals("Formats")) {
if (calendarFormatTypes.contains(fields[1])) {
order += calendarFormatTypes.indexOf(fields[1]) * 10;
} else {
order += calendarFormatTypes.size() * 10;
}
if (calendarFormatSubtypes.contains(fields[2])) {
order += calendarFormatSubtypes.indexOf(fields[2]);
} else {
order += calendarFormatSubtypes.size();
}
} else {
if (widthValues.contains(fields[1])) {
order += widthValues.indexOf(fields[1]) * 10;
} else {
order += widthValues.size() * 10;
}
if (calendarContextTypes.contains(fields[2])) {
order += calendarContextTypes.indexOf(fields[2]);
} else {
order += calendarContextTypes.size();
}
}
String[] fixedFields = new String[fields.length];
for (int i = 0; i < fields.length; i++) {
String s = fixNames.get(fields[i]);
fixedFields[i] = s != null ? s : fields[i];
}
return fixedFields[0]
+ " - "
+ fixedFields[1]
+ (fixedFields[2].length() > 0 ? " - " + fixedFields[2] : "");
}
});
functionMap.put(
"titlecase",
new Transform<>() {
@Override
public String transform(String source) {
return UCharacter.toTitleCase(source, null);
}
});
functionMap.put(
"categoryFromScript",
new Transform<>() {
@Override
public String transform(String source) {
String script = hyphenSplitter.split(source);
Info info = ScriptMetadata.getInfo(script);
if (info == null) {
info = ScriptMetadata.getInfo("Zzzz");
}
order = 100 - info.idUsage.ordinal();
return info.idUsage.name;
}
});
functionMap.put(
"categoryFromKey",
new Transform<>() {
final Map<String, String> fixNames =
Builder.with(new HashMap<String, String>())
.put("cf", "Currency Format")
.put("em", "Emoji Presentation")
.put("fw", "First Day of Week")
.put("lb", "Line Break")
.put("hc", "Hour Cycle")
.put("ms", "Measurement System")
.put("ss", "Sentence Break Suppressions")
.freeze();
@Override
public String transform(String source) {
String fixedName = fixNames.get(source);
return fixedName != null ? fixedName : source;
}
});
functionMap.put(
"languageSection",
new Transform<>() {
final char[] languageRangeStartPoints = {'A', 'E', 'K', 'O', 'T'};
final char[] languageRangeEndPoints = {'D', 'J', 'N', 'S', 'Z'};
@Override
public String transform(String source0) {
char firstLetter = getEnglishFirstLetter(source0).charAt(0);
for (int i = 0; i < languageRangeStartPoints.length; i++) {
if (firstLetter >= languageRangeStartPoints[i]
&& firstLetter <= languageRangeEndPoints[i]) {
return "Languages ("
+ Character.toUpperCase(languageRangeStartPoints[i])
+ "-"
+ Character.toUpperCase(languageRangeEndPoints[i])
+ ")";
}
}
return "Languages";
}
});
functionMap.put(
"firstLetter",
new Transform<>() {
@Override
public String transform(String source0) {
return getEnglishFirstLetter(source0);
}
});
functionMap.put(
"languageSort",
new Transform<>() {
@Override
public String transform(String source0) {
String languageOnlyPart;
int underscorePos = source0.indexOf("_");
if (underscorePos > 0) {
languageOnlyPart = source0.substring(0, underscorePos);
} else {
languageOnlyPart = source0;
}
return englishFile.getName(CLDRFile.LANGUAGE_NAME, languageOnlyPart)
+ " \u25BA "
+ source0;
}
});
functionMap.put(
"scriptFromLanguage",
new Transform<>() {
@Override
public String transform(String source0) {
String language = hyphenSplitter.split(source0);
String script = likelySubtags.getLikelyScript(language);
if (script == null) {
script = likelySubtags.getLikelyScript(language);
}
String scriptName = englishFile.getName(CLDRFile.SCRIPT_NAME, script);
return "Languages in "
+ (script.equals("Hans") || script.equals("Hant")
? "Han Script"
: scriptName.endsWith(" Script")
? scriptName
: scriptName + " Script");
}
});
functionMap.put(
"categoryFromTerritory",
catFromTerritory =
new Transform<>() {
@Override
public String transform(String source) {
String territory = getSubdivisionsTerritory(source, null);
String container = Containment.getContainer(territory);
order = Containment.getOrder(territory);
return englishFile.getName(CLDRFile.TERRITORY_NAME, container);
}
});
functionMap.put(
"territorySection",
new Transform<>() {
final Set<String> specialRegions =
new HashSet<>(Arrays.asList("EZ", "EU", "QO", "UN", "ZZ"));
@Override
public String transform(String source0) {
// support subdivisions
String theTerritory = getSubdivisionsTerritory(source0, null);
try {
if (specialRegions.contains(theTerritory)
|| theTerritory.charAt(0) < 'A'
&& Integer.parseInt(theTerritory) > 0) {
return "Geographic Regions";
}
} catch (NumberFormatException ex) {
}
String theContinent = Containment.getContinent(theTerritory);
String theSubContinent;
switch (theContinent) { // was Integer.valueOf
case "019": // Americas - For the territorySection, we just group
// North America & South America
final String subcontinent =
Containment.getSubcontinent(theTerritory);
theSubContinent =
subcontinent.equals("005")
? "005"
: "003"; // was Integer.valueOf(subcontinent) ==
// 5
return "Territories ("
+ englishFile.getName(
CLDRFile.TERRITORY_NAME, theSubContinent)
+ ")";
case "001":
case "ZZ":
return "Geographic Regions"; // not in containment
default:
return "Territories ("
+ englishFile.getName(
CLDRFile.TERRITORY_NAME, theContinent)
+ ")";
}
}
});
functionMap.put(
"categoryFromTimezone",
catFromTimezone =
new Transform<>() {
@Override
public String transform(String source0) {
String territory = Containment.getRegionFromZone(source0);
if (territory == null) {
territory = "ZZ";
}
return catFromTerritory.transform(territory);
}
});
functionMap.put(
"timeZonePage",
new Transform<>() {
Set<String> singlePageTerritories =
new HashSet<>(Arrays.asList("AQ", "RU", "ZZ"));
@Override
public String transform(String source0) {
String theTerritory = Containment.getRegionFromZone(source0);
if (theTerritory == null
|| "001".equals(theTerritory)
|| "ZZ".equals(theTerritory)) {
if ("Etc/Unknown".equals(source0)) {
theTerritory = "ZZ";
} else {
throw new IllegalArgumentException(
"ICU needs zone update? Source: "
+ source0
+ "; Territory: "
+ theTerritory);
}
}
if (singlePageTerritories.contains(theTerritory)) {
return englishFile.getName(CLDRFile.TERRITORY_NAME, theTerritory);
}
String theContinent = Containment.getContinent(theTerritory);
final String subcontinent = Containment.getSubcontinent(theTerritory);
String theSubContinent;
switch (Integer.parseInt(theContinent)) {
case 9: // Oceania - For the timeZonePage, we group Australasia on
// one page, and the rest of Oceania on the other.
try {
theSubContinent =
subcontinent.equals("053") ? "053" : "009"; // was
// Integer.valueOf(subcontinent) ==
// 53
} catch (NumberFormatException ex) {
theSubContinent = "009";
}
return englishFile.getName(
CLDRFile.TERRITORY_NAME, theSubContinent);
case 19: // Americas - For the timeZonePage, we just group North
// America & South America
theSubContinent =
Integer.parseInt(subcontinent) == 5 ? "005" : "003";
return englishFile.getName(
CLDRFile.TERRITORY_NAME, theSubContinent);
case 142: // Asia
return englishFile.getName(
CLDRFile.TERRITORY_NAME, subcontinent);
default:
return englishFile.getName(
CLDRFile.TERRITORY_NAME, theContinent);
}
}
});
functionMap.put(
"timezoneSorting",
new Transform<>() {
@Override
public String transform(String source) {
final List<String> codeValues =
Arrays.asList(
"generic-long",
"generic-short",
"standard-long",
"standard-short",
"daylight-long",
"daylight-short");
if (codeValues.contains(source)) {
order = codeValues.indexOf(source);
} else {
order = codeValues.size();
}
return source;
}
});
functionMap.put(
"tzdpField",
new Transform<>() {
@Override
public String transform(String source) {
Map<String, String> fieldNames =
Builder.with(new HashMap<String, String>())
.put("regionFormat", "Region Format - Generic")
.put(
"regionFormat-standard",
"Region Format - Standard")
.put(
"regionFormat-daylight",
"Region Format - Daylight")
.put("gmtFormat", "GMT Format")
.put("hourFormat", "GMT Hours/Minutes Format")
.put("gmtZeroFormat", "GMT Zero Format")
.put("fallbackFormat", "Location Fallback Format")
.freeze();
final List<String> fieldOrder =
Arrays.asList(
"regionFormat",
"regionFormat-standard",
"regionFormat-daylight",
"gmtFormat",
"hourFormat",
"gmtZeroFormat",
"fallbackFormat");
if (fieldOrder.contains(source)) {
order = fieldOrder.indexOf(source);
} else {
order = fieldOrder.size();
}
String result = fieldNames.get(source);
return result == null ? source : result;
}
});
functionMap.put(
"unit",
new Transform<>() {
@Override
public String transform(String source) {
int m = unitOrder.indexOf(source);
order = m;
return source.substring(source.indexOf('-') + 1);
}
});
functionMap.put(
"numericSort",
new Transform<>() {
// Probably only works well for small values, like -5 through +4.
@Override
public String transform(String source) {
Integer pos = Integer.parseInt(source) + 5;
suborder = new SubstringOrder(pos.toString());
return source;
}
});
functionMap.put(
"metazone",
new Transform<>() {
@Override
public String transform(String source) {
if (PathHeader.UNIFORM_CONTINENTS) {
String container = getMetazonePageTerritory(source);
order = Containment.getOrder(container);
return englishFile.getName(CLDRFile.TERRITORY_NAME, container);
} else {
String continent = metazoneToContinent.get(source);
if (continent == null) {
continent = "UnknownT";
}
return continent;
}
}
});
Object[][] ctto = {
{"BUK", "MM"},
{"CSD", "RS"},
{"CSK", "CZ"},
{"DDM", "DE"},
{"EUR", "ZZ"},
{"RHD", "ZW"},
{"SUR", "RU"},
{"TPE", "TL"},
{"XAG", "ZZ"},
{"XAU", "ZZ"},
{"XBA", "ZZ"},
{"XBB", "ZZ"},
{"XBC", "ZZ"},
{"XBD", "ZZ"},
{"XDR", "ZZ"},
{"XEU", "ZZ"},
{"XFO", "ZZ"},
{"XFU", "ZZ"},
{"XPD", "ZZ"},
{"XPT", "ZZ"},
{"XRE", "ZZ"},
{"XSU", "ZZ"},
{"XTS", "ZZ"},
{"XUA", "ZZ"},
{"XXX", "ZZ"},
{"YDD", "YE"},
{"YUD", "RS"},
{"YUM", "RS"},
{"YUN", "RS"},
{"YUR", "RS"},
{"ZRN", "CD"},
{"ZRZ", "CD"},
};
Object[][] sctc = {
{"Northern America", "North America (C)"},
{"Central America", "North America (C)"},
{"Caribbean", "North America (C)"},
{"South America", "South America (C)"},
{"Northern Africa", "Northern Africa"},
{"Western Africa", "Western Africa"},
{"Middle Africa", "Middle Africa"},
{"Eastern Africa", "Eastern Africa"},
{"Southern Africa", "Southern Africa"},
{"Europe", "Northern/Western Europe"},
{"Northern Europe", "Northern/Western Europe"},
{"Western Europe", "Northern/Western Europe"},
{"Eastern Europe", "Southern/Eastern Europe"},
{"Southern Europe", "Southern/Eastern Europe"},
{"Western Asia", "Western Asia (C)"},
{"Central Asia", "Central Asia (C)"},
{"Eastern Asia", "Eastern Asia (C)"},
{"Southern Asia", "Southern Asia (C)"},
{"Southeast Asia", "Southeast Asia (C)"},
{"Australasia", "Oceania (C)"},
{"Melanesia", "Oceania (C)"},
{"Micronesian Region", "Oceania (C)"}, // HACK
{"Polynesia", "Oceania (C)"},
{"Unknown Region", "Unknown Region (C)"},
};
final Map<String, String> currencyToTerritoryOverrides = CldrUtility.asMap(ctto);
final Map<String, String> subContinentToContinent = CldrUtility.asMap(sctc);
final Set<String> fundCurrencies =
new HashSet<>(
Arrays.asList(
"CHE", "CHW", "CLF", "COU", "ECV", "MXV", "USN", "USS", "UYI",
"XEU", "ZAL"));
final Set<String> offshoreCurrencies = new HashSet<>(Arrays.asList("CNH"));
// TODO: Put this into supplementalDataInfo ?
functionMap.put(
"categoryFromCurrency",
new Transform<>() {
@Override
public String transform(String source0) {
String tenderOrNot = "";
String territory =
likelySubtags.getLikelyTerritoryFromCurrency(source0);
if (territory == null) {
String tag;
if (fundCurrencies.contains(source0)) {
tag = " (fund)";
} else if (offshoreCurrencies.contains(source0)) {
tag = " (offshore)";
} else {
tag = " (old)";
}
tenderOrNot = ": " + source0 + tag;
}
if (currencyToTerritoryOverrides.keySet().contains(source0)) {
territory = currencyToTerritoryOverrides.get(source0);
} else if (territory == null) {
territory = source0.substring(0, 2);
}
if (territory.equals("ZZ")) {
order = 999;
return englishFile.getName(CLDRFile.TERRITORY_NAME, territory)
+ ": "
+ source0;
} else {
return catFromTerritory.transform(territory)
+ ": "
+ englishFile.getName(CLDRFile.TERRITORY_NAME, territory)
+ tenderOrNot;
}
}
});
functionMap.put(
"continentFromCurrency",
new Transform<>() {
@Override
public String transform(String source0) {
String subContinent;
String territory =
likelySubtags.getLikelyTerritoryFromCurrency(source0);
if (currencyToTerritoryOverrides.keySet().contains(source0)) {
territory = currencyToTerritoryOverrides.get(source0);
} else if (territory == null) {
territory = source0.substring(0, 2);
}
if (territory.equals("ZZ")) {
order = 999;
subContinent =
englishFile.getName(CLDRFile.TERRITORY_NAME, territory);
} else {
subContinent = catFromTerritory.transform(territory);
}
String result =
subContinentToContinent.get(
subContinent); // the continent is the last word in the
// territory representation
return result;
}
});
functionMap.put(
"numberingSystem",
new Transform<>() {
@Override
public String transform(String source0) {
if ("latn".equals(source0)) {
return "";
}
String displayName =
englishFile.getStringValue(
"//ldml/localeDisplayNames/types/type[@key=\"numbers\"][@type=\""
+ source0
+ "\"]");
return "using "
+ (displayName == null
? source0
: displayName + " (" + source0 + ")");
}
});
functionMap.put(
"datefield",
new Transform<>() {
private final String[] datefield = {
"era", "era-short", "era-narrow",
"century", "century-short", "century-narrow",
"year", "year-short", "year-narrow",
"quarter", "quarter-short", "quarter-narrow",
"month", "month-short", "month-narrow",
"week", "week-short", "week-narrow",
"weekOfMonth", "weekOfMonth-short", "weekOfMonth-narrow",
"day", "day-short", "day-narrow",
"dayOfYear", "dayOfYear-short", "dayOfYear-narrow",
"weekday", "weekday-short", "weekday-narrow",
"weekdayOfMonth", "weekdayOfMonth-short", "weekdayOfMonth-narrow",
"dayperiod", "dayperiod-short", "dayperiod-narrow",
"zone", "zone-short", "zone-narrow",
"hour", "hour-short", "hour-narrow",
"minute", "minute-short", "minute-narrow",
"second", "second-short", "second-narrow",
"millisecond", "millisecond-short", "millisecond-narrow",
"microsecond", "microsecond-short", "microsecond-narrow",
"nanosecond", "nanosecond-short", "nanosecond-narrow",
};
@Override
public String transform(String source) {
order = getIndex(source, datefield);
return source;
}
});
// //ldml/dates/fields/field[@type="%A"]/relative[@type="%A"]
functionMap.put(
"relativeDate",
new Transform<>() {
private final String[] relativeDateField = {
"year", "year-short", "year-narrow",
"quarter", "quarter-short", "quarter-narrow",
"month", "month-short", "month-narrow",
"week", "week-short", "week-narrow",
"day", "day-short", "day-narrow",
"hour", "hour-short", "hour-narrow",
"minute", "minute-short", "minute-narrow",
"second", "second-short", "second-narrow",
"sun", "sun-short", "sun-narrow",
"mon", "mon-short", "mon-narrow",
"tue", "tue-short", "tue-narrow",
"wed", "wed-short", "wed-narrow",
"thu", "thu-short", "thu-narrow",
"fri", "fri-short", "fri-narrow",
"sat", "sat-short", "sat-narrow",
};
private final String[] longNames = {
"Year", "Year Short", "Year Narrow",
"Quarter", "Quarter Short", "Quarter Narrow",
"Month", "Month Short", "Month Narrow",
"Week", "Week Short", "Week Narrow",
"Day", "Day Short", "Day Narrow",
"Hour", "Hour Short", "Hour Narrow",
"Minute", "Minute Short", "Minute Narrow",
"Second", "Second Short", "Second Narrow",
"Sunday", "Sunday Short", "Sunday Narrow",
"Monday", "Monday Short", "Monday Narrow",
"Tuesday", "Tuesday Short", "Tuesday Narrow",
"Wednesday", "Wednesday Short", "Wednesday Narrow",
"Thursday", "Thursday Short", "Thursday Narrow",
"Friday", "Friday Short", "Friday Narrow",
"Saturday", "Saturday Short", "Saturday Narrow",
};
@Override
public String transform(String source) {
order = getIndex(source, relativeDateField) + 100;
return "Relative " + longNames[getIndex(source, relativeDateField)];
}
});
// Sorts numberSystem items (except for decimal formats).
functionMap.put(
"number",
new Transform<>() {
private final String[] symbols = {
"decimal",
"group",
"plusSign",
"minusSign",
"approximatelySign",
"percentSign",
"perMille",
"exponential",
"superscriptingExponent",
"infinity",
"nan",
"list",
"currencies"
};
@Override
public String transform(String source) {
String[] parts = source.split("-");
order = getIndex(parts[0], symbols);
// e.g. "currencies-one"
if (parts.length > 1) {
suborder = new SubstringOrder(parts[1]);
}
return source;
}
});
functionMap.put(
"numberFormat",
new Transform<>() {
@Override
public String transform(String source) {
final List<String> fieldOrder =
Arrays.asList(
"standard-decimal",
"standard-currency",
"standard-currency-accounting",
"standard-percent",
"standard-scientific");
if (fieldOrder.contains(source)) {
order = fieldOrder.indexOf(source);
} else {
order = fieldOrder.size();
}
return source;
}
});
functionMap.put(
"localePattern",
new Transform<>() {
@Override
public String transform(String source) {
// Put localeKeyTypePattern behind localePattern and
// localeSeparator.
if (source.equals("localeKeyTypePattern")) {
order = 10;
}
return source;
}
});
functionMap.put(
"listOrder",
new Transform<>() {
private String[] listParts = {"2", "start", "middle", "end"};
@Override
public String transform(String source) {
order = getIndex(source, listParts);
return source;
}
});
functionMap.put(
"personNameSection",
new Transform<>() {
@Override
public String transform(String source) {
// sampleName item values in desired sort order
final List<String> itemValues =
PersonNameFormatter.SampleType.ALL_STRINGS;
if (source.equals("NameOrder")) {
order = 0;
return "NameOrder for Locales";
}
if (source.equals("Parameters")) {
order = 4;
return "Default Parameters";
}
if (source.equals("AuxiliaryItems")) {
order = 10;
return source;
}
String itemPrefix = "SampleName:";
if (source.startsWith(itemPrefix)) {
String itemValue = source.substring(itemPrefix.length());
order = 20 + itemValues.indexOf(itemValue);
return "SampleName Fields for Item: " + itemValue;
}
String pnPrefix = "PersonName:";
if (source.startsWith(pnPrefix)) {
String attrValues = source.substring(pnPrefix.length());
List<String> parts = HYPHEN_SPLITTER.splitToList(attrValues);
String nameOrder = parts.get(0);
if (nameOrder.contentEquals("sorting")) {
order = 40;
return "PersonName Sorting Patterns (Usage: referring)";
}
order = 30;
if (nameOrder.contentEquals("surnameFirst")) {
order += 1;
}
String nameUsage = parts.get(1);
if (nameUsage.contentEquals("monogram")) {
order += 20;
return "PersonName Monogram Patterns for Order: " + nameOrder;
}
return "PersonName Main Patterns for Order: " + nameOrder;
}
order = 60;
return source;
}
});
functionMap.put(
"personNameOrder",
new Transform<>() {
@Override
public String transform(String source) {
// personName attribute values: each group in desired
// sort order, but groups from least important to most
final List<String> attrValues =
Arrays.asList(
"referring",
"addressing", // usage values to include
"formal",
"informal", // formality values
"long",
"medium",
"short"); // length values
// order & length values handled in &personNameSection
List<String> parts = HYPHEN_SPLITTER.splitToList(source);
order = 0;
String attributes = "";
boolean skipReferring = false;
for (String part : parts) {
if (attrValues.contains(part)) {
order += (1 << attrValues.indexOf(part));
// anything else like alt="variant" is at order 0
if (!skipReferring || !part.contentEquals("referring")) {
// Add this part to display attribute string
if (attributes.length() == 0) {
attributes = part;
} else {
attributes = attributes + "-" + part;
}
}
} else if (part.contentEquals("sorting")) {
skipReferring = true; // For order=sorting, don't display
// usage=referring
}
}
return attributes;
}
});
functionMap.put(
"sampleNameOrder",
new Transform<>() {
@Override
public String transform(String source) {
// The various nameField attribute values: each group in desired
// sort order, but groups from least important to most
final List<String> attrValues =
Arrays.asList(
"informal",
"prefix",
"core", // modifiers for nameField type
"prefix",
"given",
"given2",
"surname",
"surname2",
"suffix"); // values for nameField type
List<String> parts = HYPHEN_SPLITTER.splitToList(source);
order = 0;
for (String part : parts) {
if (attrValues.contains(part)) {
order += (1 << attrValues.indexOf(part));
} // anything else like alt="variant" is at order 0
}
return source;
}
});
functionMap.put(
"alphaOrder",
new Transform<>() {
@Override
public String transform(String source) {
order = 0;
return source;
}
});
functionMap.put(
"transform",
new Transform<>() {
Splitter commas = Splitter.on(',').trimResults();
@Override
public String transform(String source) {
List<String> parts = commas.splitToList(source);
return parts.get(1)
+ (parts.get(0).equals("both") ? "↔︎" : "→")
+ parts.get(2)
+ (parts.size() > 3 ? "/" + parts.get(3) : "");
}
});
functionMap.put(
"major",
new Transform<>() {
@Override
public String transform(String source) {
return getCharacterPageId(source).toString();
}
});
functionMap.put(
"minor",
new Transform<>() {
@Override
public String transform(String source) {
String minorCat = Emoji.getMinorCategory(source);
order = Emoji.getEmojiMinorOrder(minorCat);
return minorCat;
}
});
/**
* Use the ordering of the emoji in getEmojiToOrder rather than alphabetic, since the
* collator data won't be ready until the candidates are final.
*/
functionMap.put(
"emoji",
new Transform<>() {
@Override
public String transform(String source) {
int dashPos = source.indexOf(' ');
String emoji = source.substring(0, dashPos);
order =
(Emoji.getEmojiToOrder(emoji) << 1)
+ (source.endsWith("name") ? 0 : 1);
return source;
}
});
}
private static int getIndex(String item, String[] array) {
for (int i = 0; i < array.length; i++) {
if (item.equals(array[i])) {
return i;
}
}
return -1;
}
private static String getEnglishFirstLetter(String s) {
String languageOnlyPart;
int underscorePos = s.indexOf("_");
if (underscorePos > 0) {
languageOnlyPart = s.substring(0, underscorePos);
} else {
languageOnlyPart = s;
}
final String name = englishFile.getName(CLDRFile.LANGUAGE_NAME, languageOnlyPart);
return name == null ? "?" : name.substring(0, 1).toUpperCase();
}
static class HyphenSplitter {
String main;
String extras;
String split(String source) {
int hyphenPos = source.indexOf('-');
if (hyphenPos < 0) {
main = source;
extras = "";
} else {
main = source.substring(0, hyphenPos);
extras = source.substring(hyphenPos);
}
return main;
}
}
/**
* This converts "functions", like &month, and sets the order.
*
* @param input
* @param orderIn
* @return
*/
private static String fix(String input, int orderIn) {
input = RegexLookup.replace(input, args.value);
order = orderIn;
suborder = null;
int pos = 0;
while (true) {
int functionStart = input.indexOf('&', pos);
if (functionStart < 0) {
if ("Volume".equals(input)) {
return getVolumePageId(args.value[0] /* path */).toString();
}
return input;
}
int functionEnd = input.indexOf('(', functionStart);
int argEnd =
input.indexOf(
')', functionEnd + 2); // we must insert at least one character
Transform<String, String> func =
functionMap.get(input.substring(functionStart + 1, functionEnd));
final String arg = input.substring(functionEnd + 1, argEnd);
String temp = func.transform(arg);
if (temp == null) {
func.transform(arg);
throw new IllegalArgumentException(
"Function returns invalid results for «" + arg + "».");
}
input = input.substring(0, functionStart) + temp + input.substring(argEnd + 1);
pos = functionStart + temp.length();
}
}
private static Set<UnitConverter.UnitSystem> METRIC =
Set.of(UnitConverter.UnitSystem.metric, UnitConverter.UnitSystem.metric_adjacent);
private static PageId getVolumePageId(String path) {
// Extract the unit from the path. For example, if path is
// //ldml/units/unitLength[@type="narrow"]/unit[@type="volume-cubic-kilometer"]/displayName
// then extract "volume-cubic-kilometer" which is the long unit id
final String longUnitId =
XPathParts.getFrozenInstance(path).findAttributeValue("unit", "type");
if (longUnitId == null) {
throw new InternalCldrException("Missing unit in path " + path);
}
final UnitConverter uc = supplementalDataInfo.getUnitConverter();
// Convert, for example, "volume-cubic-kilometer" to "cubic-kilometer"
final String shortUnitId = uc.getShortId(longUnitId);
if (!Collections.disjoint(METRIC, uc.getSystemsEnum(shortUnitId))) {
return PageId.Volume_Metric;
} else {
return PageId.Volume_Other;
}
}
/**
* Collect all the paths for a CLDRFile, and make sure that they have cached PathHeaders
*
* @param file
* @return immutable set of paths in the file
*/
public Set<String> pathsForFile(CLDRFile file) {
// make sure we cache all the path headers
HashSet<String> filePaths = new HashSet<>();
file.fullIterable().forEach(filePaths::add);
for (String path : filePaths) {
try {
fromPath(path); // call to make sure cached
} catch (Throwable t) {
// ... some other exception
}
}
return Collections.unmodifiableSet(filePaths);
}
/**
* Returns those regexes that were never matched.
*
* @return
*/
public Set<String> getUnmatchedRegexes() {
Map<String, RawData> outputUnmatched = new LinkedHashMap<>();
lookup.getUnmatchedPatterns(matchersFound, outputUnmatched);
return outputUnmatched.keySet();
}
}
/**
* Return the territory used for the title of the Metazone page in the Survey Tool.
*
* @param source
* @return
*/
public static String getMetazonePageTerritory(String source) {
String result = metazoneToPageTerritory.get(source);
return result == null ? "ZZ" : result;
}
private static final List<String> COUNTS =
Arrays.asList("displayName", "zero", "one", "two", "few", "many", "other", "per");
private static Collator alphabetic;
private static int alphabeticCompare(String aa, String bb) {
if (alphabetic == null) {
initializeAlphabetic();
}
return alphabetic.compare(aa, bb);
}
private static synchronized void initializeAlphabetic() {
// Lazy initialization: don't call CLDRConfig.getInstance() too early or we'll get
// "CLDRConfig.getInstance() was called prior to SurveyTool setup" when called from
// com.ibm.ws.microprofile.openapi.impl.core.jackson.ModelResolver._addEnumProps
if (alphabetic == null) {
alphabetic = CLDRConfig.getInstance().getCollatorRoot();
}
}
/**
* @deprecated use CLDRConfig.getInstance().urls() instead
*/
@Deprecated
public enum BaseUrl {
// http://st.unicode.org/smoketest/survey?_=af&strid=55053dffac611328
// http://st.unicode.org/cldr-apps/survey?_=en&strid=3cd31261bf6738e1
SMOKE("https://st.unicode.org/smoketest/survey"),
PRODUCTION("https://st.unicode.org/cldr-apps/survey");
final String base;
private BaseUrl(String url) {
base = url;
}
}
/**
* @deprecated, use CLDRConfig.urls().forPathHeader() instead.
* @param baseUrl
* @param locale
* @return
*/
public String getUrl(BaseUrl baseUrl, String locale) {
return getUrl(baseUrl.base, locale);
}
/**
* @deprecated, use CLDRConfig.urls().forPathHeader() instead.
* @param baseUrl
* @param locale
* @return
*/
public String getUrl(String baseUrl, String locale) {
return getUrl(baseUrl, locale, getOriginalPath());
}
/**
* Map http://st.unicode.org/smoketest/survey to http://st.unicode.org/smoketest etc
*
* @param str
* @return
*/
public static String trimLast(String str) {
int n = str.lastIndexOf('/');
if (n == -1) return "";
return str.substring(0, n + 1);
}
public static String getUrlForLocalePath(String locale, String path) {
return getUrl(SURVEY_URL, locale, path);
}
public static String getUrl(String baseUrl, String locale, String path) {
return trimLast(baseUrl) + "v#/" + locale + "//" + StringId.getHexId(path);
}
/**
* @deprecated use the version with CLDRURLS instead
* @param baseUrl
* @param file
* @param path
* @return
*/
@Deprecated
public static String getLinkedView(String baseUrl, CLDRFile file, String path) {
return SECTION_LINK
+ PathHeader.getUrl(baseUrl, file.getLocaleID(), path)
+ "'><em>view</em></a>";
}
public static String getLinkedView(CLDRURLS urls, CLDRFile file, String path) {
return SECTION_LINK + urls.forXpath(file.getLocaleID(), path) + "'><em>view</em></a>";
}
private static final String SURVEY_URL = CLDRConfig.getInstance().urls().base();
/**
* If a subdivision, return the (uppercased) territory and if suffix != null, the suffix.
* Otherwise return the input as is.
*
* @param input
* @param suffix
* @return
*/
private static String getSubdivisionsTerritory(String input, Output<String> suffix) {
String theTerritory;
if (StandardCodes.LstrType.subdivision.isWellFormed(input)) {
int territoryEnd = input.charAt(0) < 'A' ? 3 : 2;
theTerritory = input.substring(0, territoryEnd).toUpperCase(Locale.ROOT);
if (suffix != null) {
suffix.value = input.substring(territoryEnd);
}
} else {
theTerritory = input;
if (suffix != null) {
suffix.value = "";
}
}
return theTerritory;
}
/**
* Should this path header be hidden?
*
* @return true to hide, else false
*/
public boolean shouldHide() {
switch (status) {
case HIDE:
case DEPRECATED:
return true;
case READ_ONLY:
case READ_WRITE:
case LTR_ALWAYS:
return false;
default:
logger.log(java.util.logging.Level.SEVERE, "Missing case for " + status);
return false;
}
}
/**
* Are reading and writing allowed for this path header?
*
* @return true if reading and writing are allowed, else false
*/
public boolean canReadAndWrite() {
switch (status) {
case READ_WRITE:
case LTR_ALWAYS:
return true;
case HIDE:
case DEPRECATED:
case READ_ONLY:
return false;
default:
logger.log(java.util.logging.Level.SEVERE, "Missing case for " + status);
return false;
}
}
private static UnicodeMap<PageId> nonEmojiMap = null;
/**
* Return the PageId for the given character
*
* @param cp the character as a string
* @return the PageId
*/
private static PageId getCharacterPageId(String cp) {
if (Emoji.getAllRgiNoES().contains(cp)) {
return Emoji.getPageId(cp);
}
if (nonEmojiMap == null) {
nonEmojiMap = createNonEmojiMap();
}
PageId pageId = nonEmojiMap.get(cp);
if (pageId == null) {
throw new InternalCldrException("Failure getting character page id");
}
return pageId;
}
/**
* Create the map from non-emoji characters to pages. Call with lazy initialization to avoid
* static initialization bugs, otherwise PageId.OtherSymbols could still be null.
*
* @return the map from character to PageId
*/
private static UnicodeMap<PageId> createNonEmojiMap() {
return new UnicodeMap<PageId>()
.putAll(new UnicodeSet("[:P:]"), PageId.Punctuation)
.putAll(new UnicodeSet("[:Sm:]"), PageId.MathSymbols)
.putAll(new UnicodeSet("[^[:Sm:][:P:]]"), PageId.OtherSymbols)
.freeze();
}
}