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}