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;
020import org.jspecify.annotations.Nullable;
021
022import java.math.BigDecimal;
023import java.util.Arrays;
024import java.util.Collections;
025import java.util.Locale;
026import java.util.Map;
027import java.util.SortedMap;
028import java.util.SortedSet;
029import java.util.stream.Collectors;
030
031import static java.util.Objects.requireNonNull;
032
033/**
034 * Language plural cardinality forms.
035 * <p>
036 * For example, English has two: {@code 1 dog, 2 dogs}, while Welsh has many: {@code 0 cŵn, 1 ci, 2 gi, 3 chi, 4 ci}.
037 * <p>
038 * See the <a href="http://cldr.unicode.org/index/cldr-spec/plural-rules">Unicode Common Locale Data Repository</a>
039 * and its <a href="https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html">Language Plural Rules</a> for details.
040 * <p>
041 * Per the CLDR:
042 * <blockquote>
043 * These categories are only mnemonics -- the names don't necessarily imply the exact contents of the category.
044 * For example, for both English and French the number 1 has the category one (singular).
045 * <p>
046 * In English, every other number has a plural form, and is given the category other.
047 * French is similar, except that the number 0 also has the category one and not other or zero, because the form of
048 * units qualified by 0 is also singular.
049 * <p>
050 * This is worth emphasizing: A common mistake is to think that "one" is only for only the number 1.
051 * Instead, "one" is a category for any number that behaves like 1. So in some languages, for example,
052 * one → numbers that end in "1" (like 1, 21, 151) but that don't end in 11 (like "11, 111, 10311).
053 * </blockquote>
054 *
055 * @author <a href="https://revetkn.com">Mark Allen</a>
056 */
057public enum Cardinality implements LanguageForm {
058  /**
059   * Normally the form used with 0, if it is limited to numbers whose integer values end with 0.
060   * <p>
061   * For example: the Welsh {@code 0 cŵn, 0 cathod} means {@code 0 dogs, 0 cats} in English.
062   */
063  ZERO,
064  /**
065   * The form used with 1.
066   * <p>
067   * For example: the Welsh {@code 1 ci, 1 gath} means {@code 1 dog, 1 cat} in English.
068   */
069  ONE,
070  /**
071   * Normally the form used with 2, if it is limited to numbers whose integer values end with 2.
072   * <p>
073   * For example: the Welsh {@code 2 gi, 2 gath} means {@code 2 dogs, 2 cats} in English.
074   */
075  TWO,
076  /**
077   * The form that falls between {@code TWO} and {@code MANY}.
078   * <p>
079   * For example: the Welsh {@code  3 chi, 3 cath} means {@code 3 dogs, 3 cats} in English.
080   */
081  FEW,
082  /**
083   * The form that falls between {@code FEW} and {@code OTHER}.
084   * <p>
085   * For example: the Welsh {@code 6 chi, 6 chath} means {@code 6 dogs, 6 cats} in English.
086   */
087  MANY,
088  /**
089   * General "catchall" form which comprises any cases not handled by the other forms.
090   * <p>
091   * For example: the Welsh {@code 4 ci, 4 cath} means {@code 4 dogs, 4 cats} in English.
092   */
093  OTHER;
094
095  @NonNull
096  static final Map<@NonNull String, @NonNull Cardinality> CARDINALITIES_BY_NAME;
097
098  static {
099    CARDINALITIES_BY_NAME = Collections.unmodifiableMap(Arrays.stream(
100        Cardinality.values()).collect(Collectors.toMap(cardinality -> cardinality.name(), cardinality -> cardinality)));
101  }
102
103  /**
104   * Gets an appropriate plural cardinality for the given number and locale.
105   * <p>
106   * Negative numbers are evaluated using their absolute value.
107   * <p>
108   * When determining cardinality, the decimal places of {@code number} will be computed and used.
109   * Note that if trailing zeroes are important, e.g. {@code 1.00} instead of {@code 1}, you must either specify a {@link BigDecimal} with appropriate
110   * scale or supply a non-null {@code visibleDecimalPlaces} value.
111   * <p>
112   * If you do not provide a {@link BigDecimal} and wish to manually specify the number of visible decimals, use {@link #forNumber(Number, Integer, Locale)} instead.
113   * Supported {@link Number} implementations and their conversion semantics are documented by
114   * {@link PluralOperands#forNumber(Number)}.
115   * This convenience method uses {@link TranslationRuntimeLimits#defaults()}. To apply different limits, construct
116   * {@link PluralOperands} with {@link PluralOperands.Builder#runtimeLimits(TranslationRuntimeLimits)} and call
117   * {@link #forOperands(PluralOperands, Locale)}.
118   * <p>
119   * See the <a href="https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html">CLDR Language Plural Rules</a>
120   * for further details.
121   *
122   * @param number the number that drives pluralization, not null
123   * @param locale the locale that drives pluralization, not null
124   * @return an appropriate plural cardinality, not null
125   * @throws UnsupportedLocaleException if the locale is not supported
126   * @throws IllegalArgumentException if the locale is malformed, the number implementation is unsupported, the
127   *                                  number is non-finite, or the number exceeds the safety limits documented by
128   *                                  {@link PluralOperands}
129   */
130  @NonNull
131  public static Cardinality forNumber(@NonNull Number number, @NonNull Locale locale) {
132    requireNonNull(number);
133    requireNonNull(locale);
134
135    return forNumber(number, null, locale);
136  }
137
138  /**
139   * Gets an appropriate plural cardinality for the given number, visible decimal places, and locale.
140   * <p>
141   * Negative numbers are evaluated using their absolute value.
142   * <p>
143   * If {@code visibleDecimalPlaces} is null, then the decimal places of {@code number} will be computed and used.
144   * Note that if trailing zeroes are important, e.g. {@code 1.00} instead of {@code 1}, you must either specify a {@link BigDecimal} with appropriate
145   * scale or supply a non-null {@code visibleDecimalPlaces} value.
146   * Reducing the number's scale does not round implicitly; callers must supply an already-rounded value.
147   * Supported {@link Number} implementations and their conversion semantics are documented by
148   * {@link PluralOperands#forNumber(Number)}.
149   * This convenience method uses {@link TranslationRuntimeLimits#defaults()}. To apply different limits, construct
150   * {@link PluralOperands} with {@link PluralOperands.Builder#runtimeLimits(TranslationRuntimeLimits)} and call
151   * {@link #forOperands(PluralOperands, Locale)}.
152   * <p>
153   * See the <a href="https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html">CLDR Language Plural Rules</a>
154   * for further details.
155   *
156   * @param number               the number that drives pluralization, not null
157   * @param visibleDecimalPlaces the number of decimal places that will ultimately be displayed, may be null
158   * @param locale               the locale that drives pluralization, not null
159   * @return an appropriate plural cardinality, not null
160   * @throws UnsupportedLocaleException if the locale is not supported
161   * @throws ArithmeticException if the requested visible decimal places would require rounding
162   * @throws IllegalArgumentException if the locale is malformed, the number implementation is unsupported, the
163   *                                  number is non-finite, or the number or visible decimal places exceeds the safety
164   *                                  limits documented by {@link PluralOperands}
165   */
166  @NonNull
167  public static Cardinality forNumber(@NonNull Number number, @Nullable Integer visibleDecimalPlaces, @NonNull Locale locale) {
168    requireNonNull(number);
169    requireNonNull(locale);
170
171    return forOperands(PluralOperands.forNumber(number).visibleDecimalPlaces(visibleDecimalPlaces).build(), locale);
172  }
173
174  /**
175   * Gets an appropriate plural cardinality for the given CLDR plural operands and locale.
176   * <p>
177   * Most applications should use {@link #forNumber(Number, Locale)}. Use this overload when the displayed number has
178   * details that are not fully represented by the Java {@link Number}, such as a compact-decimal exponent.
179   * <p>
180   * See the <a href="https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html">CLDR Language Plural Rules</a>
181   * for further details.
182   *
183   * @param operands the CLDR plural operands that drive pluralization, not null
184   * @param locale   the locale that drives pluralization, not null
185   * @return an appropriate plural cardinality, not null
186   * @throws UnsupportedLocaleException if the locale is not supported
187   * @throws IllegalArgumentException if the locale is malformed
188   * @since 3.0.0
189   */
190  @NonNull
191  public static Cardinality forOperands(@NonNull PluralOperands operands, @NonNull Locale locale) {
192    requireNonNull(operands);
193    requireNonNull(locale);
194
195    return CldrPluralRules.cardinalityForOperands(operands, locale);
196  }
197
198  /**
199   * Gets an appropriate plural cardinality for the given range (start, end) and locale.
200   * <p>
201   * For example, a range might be {@code "1-3 hours"}.
202   * <p>
203   * Note that the cardinality of the end of the range does not necessarily
204   * determine the range's cardinality.  In English, we say {@code "0–1 days"} - the value {@code 1} is {@code CARDINALITY_ONE}
205   * but the range is {@code CARDINALITY_OTHER}.
206   * <p>
207   * See the <a href="https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html">CLDR Language Plural Rules</a>
208   * for further details.
209   *
210   * @param start  the cardinality for the start of the range, not null
211   * @param end    the cardinality for the end of the range, not null
212   * @param locale the locale that drives pluralization, not null
213   * @return an appropriate plural cardinality for the range, not null
214   * @throws UnsupportedLocaleException if the locale is not supported
215   * @throws IllegalArgumentException if the locale is malformed
216   */
217  @NonNull
218  public static Cardinality forRange(@NonNull Cardinality start, @NonNull Cardinality end, @NonNull Locale locale) {
219    requireNonNull(start);
220    requireNonNull(end);
221    requireNonNull(locale);
222
223    return CldrPluralRules.cardinalityForRange(start, end, locale);
224  }
225
226  /**
227   * Gets the set of cardinalities supported for the given locale.
228   * <p>
229   * The empty set will be returned if the locale is not supported.
230   * <p>
231   * The set's values are sorted by the natural ordering of the {@link Cardinality} enumeration.
232   *
233   * @param locale the locale to use for lookup, not null
234   * @return the cardinalities supported by the given locale, not null
235   * @throws IllegalArgumentException if the locale is malformed
236   */
237  @NonNull
238  public static SortedSet<@NonNull Cardinality> supportedCardinalitiesForLocale(@NonNull Locale locale) {
239    requireNonNull(locale);
240    return CldrPluralRules.supportedCardinalitiesForLocale(locale);
241  }
242
243  /**
244   * Gets a mapping of cardinalities to example integer values for the given locale.
245   * <p>
246   * The empty map will be returned if the locale is not supported or if no example values are available.
247   * <p>
248   * The map's keys are sorted by the natural ordering of the {@link Cardinality} enumeration.
249   *
250   * @param locale the locale to use for lookup, not null
251   * @return a mapping of cardinalities to example integer values, not null
252   * @throws IllegalArgumentException if the locale is malformed
253   */
254  @NonNull
255  public static SortedMap<@NonNull Cardinality, @NonNull Range<@NonNull Integer>> exampleIntegerValuesForLocale(@NonNull Locale locale) {
256    requireNonNull(locale);
257    return CldrPluralRules.cardinalityIntegerExamplesForLocale(locale);
258  }
259
260  /**
261   * Gets a mapping of cardinalities to example decimal values for the given locale.
262   * <p>
263   * The empty map will be returned if the locale is not supported or if no example values are available.
264   * <p>
265   * The map's keys are sorted by the natural ordering of the {@link Cardinality} enumeration.
266   *
267   * @param locale the locale to use for lookup, not null
268   * @return a mapping of cardinalities to example decimal values, not null
269   * @throws IllegalArgumentException if the locale is malformed
270   */
271  @NonNull
272  public static SortedMap<@NonNull Cardinality, @NonNull Range<@NonNull BigDecimal>> exampleDecimalValuesForLocale(@NonNull Locale locale) {
273    requireNonNull(locale);
274    return CldrPluralRules.cardinalityDecimalExamplesForLocale(locale);
275  }
276
277  /**
278   * Gets the BCP 47 locale tags represented directly in the generated CLDR cardinality-rule data.
279   * <p>
280   * This is not an exhaustive list of concrete locale tags accepted by cardinality operations. A locale with
281   * additional region or script subtags may be supported through fallback to a less-specific rule tag. Use
282   * {@link #supportedCardinalitiesForLocale(Locale)} to check a concrete locale.
283   * <p>
284   * The set's values are sorted by natural string ordering.
285   *
286   * @return the BCP 47 locale tags represented directly in the generated CLDR cardinality-rule data, not null
287   * @since 3.0.0
288   */
289  @NonNull
290  public static SortedSet<@NonNull String> getSupportedLocaleTags() {
291    return CldrPluralRules.cardinalitySupportedLocales();
292  }
293
294  /**
295   * Gets the mapping of cardinality names to values.
296   *
297   * @return the mapping of cardinality names to values, not null
298   */
299  @NonNull
300  static Map<@NonNull String, @NonNull Cardinality> getCardinalitiesByName() {
301    return CARDINALITIES_BY_NAME;
302  }
303}