001/*
002 * Copyright 2017-2022 Product Mog LLC, 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.lokalized;
018
019import org.jspecify.annotations.NonNull;
020
021import java.util.Arrays;
022import java.util.Collections;
023import java.util.Locale;
024import java.util.Map;
025import java.util.SortedMap;
026import java.util.SortedSet;
027import java.util.stream.Collectors;
028
029import static java.util.Objects.requireNonNull;
030
031/**
032 * Language plural ordinality forms.
033 * <p>
034 * For example, English has four: {@code 1st, 2nd, 3rd, 4th}, while Swedish has two: {@code 1:a, 3:e}.
035 * <p>
036 * See the <a href="http://cldr.unicode.org/index/cldr-spec/plural-rules">Unicode Common Locale Data Repository</a>
037 * and its <a href="https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html">Language Plural Rules</a> for details.
038 * <p>
039 * CLDR category names are mnemonics whose membership is locale-specific; they do not imply cardinal singular or plural
040 * meaning. For English ordinals, values ending in 1, 2, or 3 normally select {@link #ONE}, {@link #TWO}, or
041 * {@link #FEW}, except for values ending in 11, 12, or 13; remaining values select {@link #OTHER}. French ordinals use
042 * {@link #ONE} for 1 and {@link #OTHER} for other values. Applications should always use the generated locale rules
043 * rather than inferring ordinal categories from their names.
044 *
045 * @author <a href="https://revetkn.com">Mark Allen</a>
046 */
047public enum Ordinality implements LanguageForm {
048  /**
049   * Normally the form used with 0, if it is limited to numbers whose integer values end with 0.
050   * <p>
051   * For example: the Welsh {@code 0fed ci} means "{@code 0th dog}" in English.
052   */
053  ZERO,
054  /**
055   * The form used with 1.
056   * <p>
057   * For example: the Welsh {@code ci 1af} means {@code 1st dog} in English.
058   */
059  ONE,
060  /**
061   * Normally the form used with 2, if it is limited to numbers whose integer values end with 2.
062   * <p>
063   * For example: the Welsh {@code 2il gi} means {@code 2nd dog} in English.
064   */
065  TWO,
066  /**
067   * The form that falls between {@code TWO} and {@code MANY}.
068   * <p>
069   * For example: the Welsh {@code 3ydd ci} means {@code 3rd dog} in English.
070   */
071  FEW,
072  /**
073   * The form that falls between {@code FEW} and {@code OTHER}.
074   * <p>
075   * For example: the Welsh {@code 5ed ci} means {@code 5th dog} in English.
076   */
077  MANY,
078  /**
079   * General "catchall" form which comprises any cases not handled by the other forms.
080   * <p>
081   * For example: the Welsh {@code ci rhif 10} means {@code 10th dog} in English.
082   */
083  OTHER;
084
085  @NonNull
086  static final Map<@NonNull String, @NonNull Ordinality> ORDINALITIES_BY_NAME;
087
088  static {
089    ORDINALITIES_BY_NAME = Collections.unmodifiableMap(Arrays.stream(
090        Ordinality.values()).collect(Collectors.toMap(ordinality -> ordinality.name(), ordinality -> ordinality)));
091  }
092
093  /**
094   * Gets an appropriate plural ordinality for the given number and locale.
095   * <p>
096   * Negative numbers are evaluated using their absolute value.
097   * Supported {@link Number} implementations and their conversion semantics are documented by
098   * {@link PluralOperands#forNumber(Number)}.
099   * This convenience method uses {@link TranslationRuntimeLimits#defaults()}. To apply different limits, construct
100   * {@link PluralOperands} with {@link PluralOperands.Builder#runtimeLimits(TranslationRuntimeLimits)} and call
101   * {@link #forOperands(PluralOperands, Locale)}.
102   * <p>
103   * See <a href="https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html">CLDR 48 Language Plural Rules</a>
104   * for a cheat sheet.
105   *
106   * @param number the number that drives pluralization, not null
107   * @param locale the locale that drives pluralization, not null
108   * @return an appropriate plural ordinality, not null
109   * @throws UnsupportedLocaleException if the locale is not supported
110   * @throws IllegalArgumentException if the locale is malformed, the number implementation is unsupported, the
111   *                                  number is non-finite, or the number exceeds the safety limits documented by
112   *                                  {@link PluralOperands}
113   */
114  @NonNull
115  public static Ordinality forNumber(@NonNull Number number, @NonNull Locale locale) {
116    requireNonNull(number);
117    requireNonNull(locale);
118
119    return forOperands(PluralOperands.forNumber(number).build(), locale);
120  }
121
122  /**
123   * Gets an appropriate plural ordinality for the given CLDR plural operands and locale.
124   * <p>
125   * Most applications should use {@link #forNumber(Number, Locale)}. Use this overload when the displayed number has
126   * details that are not fully represented by the Java {@link Number}, such as a compact-decimal exponent.
127   * <p>
128   * See <a href="https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html">CLDR 48 Language Plural Rules</a>
129   * for a cheat sheet.
130   *
131   * @param operands the CLDR plural operands that drive pluralization, not null
132   * @param locale   the locale that drives pluralization, not null
133   * @return an appropriate plural ordinality, not null
134   * @throws UnsupportedLocaleException if the locale is not supported
135   * @throws IllegalArgumentException if the locale is malformed
136   * @since 3.0.0
137   */
138  @NonNull
139  public static Ordinality forOperands(@NonNull PluralOperands operands, @NonNull Locale locale) {
140    requireNonNull(operands);
141    requireNonNull(locale);
142
143    return CldrPluralRules.ordinalityForOperands(operands, locale);
144  }
145
146  /**
147   * Gets the set of ordinalities supported for the given locale.
148   * <p>
149   * The empty set will be returned if the locale is not supported.
150   * <p>
151   * The set's values are sorted by the natural ordering of the {@link Ordinality} enumeration.
152   *
153   * @param locale the locale to use for lookup, not null
154   * @return the ordinalities supported by the given locale, not null
155   * @throws IllegalArgumentException if the locale is malformed
156   */
157  @NonNull
158  public static SortedSet<@NonNull Ordinality> supportedOrdinalitiesForLocale(@NonNull Locale locale) {
159    requireNonNull(locale);
160    return CldrPluralRules.supportedOrdinalitiesForLocale(locale);
161  }
162
163  /**
164   * Gets a mapping of ordinalities to example integer values for the given locale.
165   * <p>
166   * The empty map will be returned if the locale is not supported or if no example values are available.
167   * <p>
168   * The map's keys are sorted by the natural ordering of the {@link Ordinality} enumeration.
169   *
170   * @param locale the locale to use for lookup, not null
171   * @return a mapping of ordinalities to example integer values, not null
172   * @throws IllegalArgumentException if the locale is malformed
173   */
174  @NonNull
175  public static SortedMap<@NonNull Ordinality, @NonNull Range<@NonNull Integer>> exampleIntegerValuesForLocale(@NonNull Locale locale) {
176    requireNonNull(locale);
177    return CldrPluralRules.ordinalityIntegerExamplesForLocale(locale);
178  }
179
180  /**
181   * Gets the BCP 47 locale tags represented directly in the generated CLDR plural-rule data and supported for
182   * ordinality operations.
183   * <p>
184   * This is not an exhaustive list of concrete locale tags accepted by ordinality operations. A locale with
185   * additional region or script subtags may be supported through fallback to a less-specific rule tag. Use
186   * {@link #supportedOrdinalitiesForLocale(Locale)} to check a concrete locale.
187   * <p>
188   * The set's values are sorted by natural string ordering.
189   *
190   * @return the directly represented BCP 47 locale tags supported for ordinality operations, not null
191   * @since 3.0.0
192   */
193  @NonNull
194  public static SortedSet<@NonNull String> getSupportedLocaleTags() {
195    return CldrPluralRules.ordinalitySupportedLocales();
196  }
197
198  /**
199   * Gets the mapping of ordinality names to values.
200   *
201   * @return the mapping of ordinality names to values, not null
202   */
203  @NonNull
204  static Map<@NonNull String, @NonNull Ordinality> getOrdinalitiesByName() {
205    return ORDINALITIES_BY_NAME;
206  }
207}