1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.core.i18n
18 
19 import java.util.regex.Pattern
20 
21 /**
22  * This class helps one create skeleton options for [DateTimeFormatter] in a safer and more
23  * discoverable manner than using raw strings.
24  *
25  * Skeletons are a flexible way to specify (in a locale independent manner) how to format of a date
26  * / time.
27  *
28  * It can be used for example to specify that a formatted date should contain a day-of-month, an
29  * abbreviated month name, and a year.
30  *
31  * It does not specify the order of the fields, or the separators, those will depend on the locale.
32  *
33  * The result will be locale dependent: "Aug 17, 2022" for English U.S., "17 Aug 2022" for English -
34  * Great Britain, "2022年8月17日" for Japanese.
35  *
36  * Skeletons are based on the
37  * [Unicode Technical Standard #35](https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table),
38  * but uses a builder to make things safer an more discoverable.
39  *
40  * You can still build these options from a string by using the
41  * [DateTimeFormatterSkeletonOptions.fromString] method.
42  */
43 public class DateTimeFormatterSkeletonOptions
44 internal constructor(
45     private val era: Era,
46     private val year: Year,
47     private val month: Month,
48     private val day: Day,
49     private val weekDay: WeekDay,
50     private val period: Period,
51     private val hour: Hour,
52     private val minute: Minute,
53     private val second: Second,
54     private val fractionalSecond: FractionalSecond,
55     private val timezone: Timezone
56 ) {
57 
58     // Date fields
59 
60     /** Era name. Era string for the date. */
61     public class Era private constructor(public val value: String) {
62         public companion object {
63             /** E.g. "Anno Domini". */
64             @JvmField public val WIDE: Era = Era("GGGG")
65 
66             /** E.g. "AD". */
67             @JvmField public val ABBREVIATED: Era = Era("G")
68 
69             /** E.g. "A". */
70             @JvmField public val NARROW: Era = Era("GGGGG")
71 
72             /** Produces no output. */
73             @JvmField public val NONE: Era = Era("")
74 
75             @JvmStatic
fromStringnull76             public fun fromString(value: String): Era {
77                 return when (value) {
78                     "G",
79                     "GG",
80                     "GGG" -> ABBREVIATED
81                     "GGGG" -> WIDE
82                     "GGGGG" -> NARROW
83                     else -> NONE
84                 }
85             }
86         }
87     }
88 
89     /** Calendar year (numeric). */
90     public class Year private constructor(public val value: String) {
91         public companion object {
92             /** As many digits as needed to show the full value. E.g. "2021" or "2009". */
93             @JvmField public val NUMERIC: Year = Year("y")
94 
95             /**
96              * The two low-order digits of the year, zero-padded as necessary. E.g. "21" or "09".
97              */
98             @JvmField public val TWO_DIGITS: Year = Year("yy")
99 
100             /** Produces no output. */
101             @JvmField public val NONE: Year = Year("")
102 
103             @JvmStatic
fromStringnull104             public fun fromString(value: String): Year {
105                 return when (value) {
106                     "y" -> NUMERIC
107                     "yy" -> TWO_DIGITS
108                     else -> NONE
109                 }
110             }
111         }
112     }
113 
114     /** Month number/name. */
115     public class Month private constructor(public val value: String) {
116         public companion object {
117             /** e.g. "September". */
118             @JvmField public val WIDE: Month = Month("MMMM")
119 
120             /** e.g. "Sep". */
121             @JvmField public val ABBREVIATED: Month = Month("MMM")
122 
123             /** Might be soo short that it is confusing. E.g. "S". */
124             @JvmField public val NARROW: Month = Month("MMMMM")
125 
126             /** E.g. "9". */
127             @JvmField public val NUMERIC: Month = Month("M")
128 
129             /** Numeric: 2 digits, zero pad if needed. May not be good i18n. E.g. "09". */
130             @JvmField public val TWO_DIGITS: Month = Month("MM")
131 
132             /** Produces no output. */
133             @JvmField public val NONE: Month = Month("")
134 
135             @JvmStatic
fromStringnull136             public fun fromString(value: String): Month {
137                 return when (value) {
138                     "M" -> NUMERIC
139                     "MM" -> TWO_DIGITS
140                     "MMM" -> ABBREVIATED
141                     "MMMM" -> WIDE
142                     "MMMMM" -> NARROW
143                     else -> NONE
144                 }
145             }
146         }
147     }
148 
149     /** Day of month (numeric). */
150     public class Day private constructor(public val value: String) {
151         public companion object {
152             /** As many digits as needed to show the full value. E.g. "1" or "17". */
153             @JvmField public val NUMERIC: Day = Day("d")
154 
155             /** Two digits, zero pad if needed. E.g. "01" or "17". */
156             @JvmField public val TWO_DIGITS: Day = Day("dd")
157 
158             /** Produces no output. */
159             @JvmField public val NONE: Day = Day("")
160 
161             @JvmStatic
fromStringnull162             public fun fromString(value: String): Day {
163                 return when (value) {
164                     "d" -> NUMERIC
165                     "dd" -> TWO_DIGITS
166                     else -> NONE
167                 }
168             }
169         }
170     }
171 
172     /** Day of week name. */
173     public class WeekDay private constructor(public val value: String) {
174         public companion object {
175             /** E.g. "Tuesday". */
176             @JvmField public val WIDE: WeekDay = WeekDay("EEEE")
177 
178             /** E.g. "Tue". */
179             @JvmField public val ABBREVIATED: WeekDay = WeekDay("E")
180 
181             /** E.g. "Tu". */
182             @JvmField public val SHORT: WeekDay = WeekDay("EEEEEE")
183 
184             /**
185              * E.g. "T". Two weekdays may have the same narrow style for some locales. E.g. the
186              * narrow style for both "Tuesday" and "Thursday" is "T".
187              */
188             @JvmField public val NARROW: WeekDay = WeekDay("EEEEE")
189 
190             /** Produces no output. */
191             @JvmField public val NONE: WeekDay = WeekDay("")
192 
193             @JvmStatic
fromStringnull194             public fun fromString(value: String): WeekDay {
195                 return when (value) {
196                     "E",
197                     "EE",
198                     "EEE" -> ABBREVIATED
199                     "EEEE" -> WIDE
200                     "EEEEE" -> NARROW
201                     "EEEEEE" -> SHORT
202                     else -> NONE
203                 }
204             }
205         }
206     }
207 
208     // Time fields
209 
210     /** The period of the day, if the hour is not 23h or 24h style. */
211     public class Period private constructor(public val value: String) {
212         public companion object {
213             /** E.g. "12 a.m.". */
214             @JvmField public val WIDE: Period = Period("aaaa")
215 
216             /** E.g. "12 a.m.". */
217             @JvmField public val ABBREVIATED: Period = Period("a")
218 
219             /** E.g. "12 a". */
220             @JvmField public val NARROW: Period = Period("aaaaa")
221 
222             /**
223              * Flexible day periods. May be upper or lowercase depending on the locale and other
224              * options. Often there is only one width that is customarily used. E.g. "3:00 at night"
225              */
226             @JvmField public val FLEXIBLE: Period = Period("B")
227 
228             /** Produces no output. */
229             @JvmField public val NONE: Period = Period("")
230 
231             @JvmStatic
fromStringnull232             public fun fromString(value: String): Period {
233                 return when (value) {
234                     "a",
235                     "aa",
236                     "aaa" -> ABBREVIATED
237                     "aaaa" -> WIDE
238                     "aaaaa" -> NARROW
239                     "B" -> FLEXIBLE
240                     else -> NONE
241                 }
242             }
243         }
244     }
245 
246     /** Hour (numeric). */
247     public class Hour private constructor(public val value: String) {
248         public companion object {
249             /**
250              * As many digits as needed to show the full value. Day period if used. E.g. "8", "8
251              * AM", "13", "1 PM".
252              */
253             @JvmField public val NUMERIC: Hour = Hour("j")
254 
255             /**
256              * Two digits, zero pad if needed. DayPeriod if used. Might be bad i18n. E.g. "08", "08
257              * AM", "13", "01 PM".
258              */
259             @JvmField public val TWO_DIGITS: Hour = Hour("jj")
260 
261             /**
262              * Bad i18n. As many digits as needed to show the full value. Day period added
263              * automatically. E.g. "8 AM", "1 PM".
264              */
265             @JvmField public val FORCE_12H_NUMERIC: Hour = Hour("h")
266 
267             /**
268              * Bad i18n. Two digits, zero pad if needed. Day period added automatically. E.g. "08
269              * AM", "01 PM".
270              */
271             @JvmField public val FORCE_12H_TWO_DIGITS: Hour = Hour("hh")
272 
273             /**
274              * Bad i18n. As many digits as needed to show the full value. No day period. E.g. "8",
275              * "13".
276              */
277             @JvmField public val FORCE_24H_NUMERIC: Hour = Hour("H")
278 
279             /** Bad i18n. Two digits, zero pad if needed. No day period. E.g. "08", "13". */
280             @JvmField public val FORCE_24H_TWO_DIGITS: Hour = Hour("HH")
281 
282             /** Produces no output. */
283             @JvmField public val NONE: Hour = Hour("")
284 
285             @JvmStatic
fromStringnull286             public fun fromString(value: String): Hour {
287                 return when (value) {
288                     "j" -> NUMERIC
289                     "jj" -> TWO_DIGITS
290                     "h" -> FORCE_12H_NUMERIC
291                     "hh" -> FORCE_12H_TWO_DIGITS
292                     "H" -> FORCE_24H_NUMERIC
293                     "HH" -> FORCE_24H_TWO_DIGITS
294                     else -> NONE
295                 }
296             }
297         }
298     }
299 
300     /** Minute (numeric). Truncated, not rounded. */
301     public class Minute private constructor(public val value: String) {
302         public companion object {
303             /** As many digits as needed to show the full value. E.g. "8", "59" */
304             @JvmField public val NUMERIC: Minute = Minute("m")
305 
306             /** Two digits, zero pad if needed. E.g. "08", "59" */
307             @JvmField public val TWO_DIGITS: Minute = Minute("mm")
308 
309             /** Produces no output. */
310             @JvmField public val NONE: Minute = Minute("")
311 
312             @JvmStatic
fromStringnull313             public fun fromString(value: String): Minute {
314                 return when (value) {
315                     "m" -> NUMERIC
316                     "mm" -> TWO_DIGITS
317                     else -> NONE
318                 }
319             }
320         }
321     }
322 
323     /** Second (numeric). Truncated, not rounded. */
324     public class Second private constructor(public val value: String) {
325         public companion object {
326             /** As many digits as needed to show the full value. E.g. "8", "59". */
327             @JvmField public val NUMERIC: Second = Second("s")
328 
329             /** Two digits, zero pad if needed. E.g. "08", "59". */
330             @JvmField public val TWO_DIGITS: Second = Second("ss")
331 
332             /** Produces no output. */
333             @JvmField public val NONE: Second = Second("")
334 
335             @JvmStatic
fromStringnull336             public fun fromString(value: String): Second {
337                 return when (value) {
338                     "s" -> NUMERIC
339                     "ss" -> TWO_DIGITS
340                     else -> NONE
341                 }
342             }
343         }
344     }
345 
346     /**
347      * Fractional Second (numeric). Truncates, like other numeric time fields, but in this case to
348      * the number of digits specified by the field length.
349      */
350     public class FractionalSecond private constructor(public val value: String) {
351         public companion object {
352             /** Fractional part represented as 3 digits. E.g. "12.345". */
353             @JvmField public val NUMERIC_3_DIGITS: FractionalSecond = FractionalSecond("SSS")
354 
355             /** Fractional part represented as 2 digits. E.g. "12.34". */
356             @JvmField public val NUMERIC_2_DIGITS: FractionalSecond = FractionalSecond("SS")
357 
358             /** Fractional part represented as 1 digit. E.g. "12.3". */
359             @JvmField public val NUMERIC_1_DIGIT: FractionalSecond = FractionalSecond("S")
360 
361             /**
362              * Fractional part dropped. Produces no output. E.g. "12" (seconds, without fractions).
363              */
364             @JvmField public val NONE: FractionalSecond = FractionalSecond("")
365 
366             @JvmStatic
fromStringnull367             public fun fromString(value: String): FractionalSecond {
368 
369                 return when (value) {
370                     "S" -> NUMERIC_1_DIGIT
371                     "SS" -> NUMERIC_2_DIGITS
372                     "SSS" -> NUMERIC_3_DIGITS
373                     else -> NONE
374                 }
375             }
376         }
377     }
378 
379     /** The localized representation of the time zone name. */
380     public class Timezone private constructor(public val value: String) {
381         public companion object {
382             /** Short localized form. E.g. "PST", "GMT-8". */
383             @JvmField public val SHORT: Timezone = Timezone("z")
384 
385             /**
386              * Long localized form. E.g. "Pacific Standard Time", "Nordamerikanische
387              * Westküsten-Normalzeit".
388              */
389             @JvmField public val LONG: Timezone = Timezone("zzzz")
390 
391             /** Short localized GMT format. E.g. "GMT-8". */
392             @JvmField public val SHORT_OFFSET: Timezone = Timezone("O")
393 
394             /** Long localized GMT format. E.g. "GMT-0800". */
395             @JvmField public val LONG_OFFSET: Timezone = Timezone("OOOO")
396 
397             /** Short generic non-location format. E.g. "PT", "Los Angeles Zeit". */
398             @JvmField public val SHORT_GENERIC: Timezone = Timezone("v")
399 
400             /**
401              * Long generic non-location format. E.g. "Pacific Time", "Nordamerikanische
402              * Westküstenzeit".
403              */
404             @JvmField public val LONG_GENERIC: Timezone = Timezone("vvvv")
405 
406             /** Produces no output. */
407             @JvmField public val NONE: Timezone = Timezone("")
408 
409             @JvmStatic
fromStringnull410             public fun fromString(value: String): Timezone {
411                 return when (value) {
412                     "z",
413                     "zz",
414                     "zzz" -> SHORT
415                     "zzzz" -> LONG
416                     "O" -> SHORT_OFFSET
417                     "OOOO" -> LONG_OFFSET
418                     "v" -> SHORT_GENERIC
419                     "vvvv" -> LONG_GENERIC
420                     else -> NONE
421                 }
422             }
423         }
424     }
425 
426     /** Returns the era option. */
getEranull427     public fun getEra(): Era = era
428 
429     /** Returns the year option. */
430     public fun getYear(): Year = year
431 
432     /** Returns the month option. */
433     public fun getMonth(): Month = month
434 
435     /** Returns the day option. */
436     public fun getDay(): Day = day
437 
438     /** Returns the day of week option. */
439     public fun getWeekDay(): WeekDay = weekDay
440 
441     /** Returns the day period option. */
442     public fun getPeriod(): Period = period
443 
444     /** Returns the hour option. */
445     public fun getHour(): Hour = hour
446 
447     /** Returns the minutes option. */
448     public fun getMinute(): Minute = minute
449 
450     /** Returns the seconds option. */
451     public fun getSecond(): Second = second
452 
453     /** Returns the fractional second option. */
454     public fun getFractionalSecond(): FractionalSecond = fractionalSecond
455 
456     /** Returns the timezone option. */
457     public fun getTimezone(): Timezone = timezone
458 
459     override fun toString(): String {
460         return era.value +
461             year.value +
462             month.value +
463             weekDay.value +
464             day.value +
465             period.value +
466             hour.value +
467             minute.value +
468             second.value +
469             fractionalSecond.value +
470             timezone.value
471     }
472 
473     public companion object {
474         // WARNING: if you change this regexp also update the switch in [fromString]
475         private val pattern =
476             Pattern.compile(
477                 "(G+)|(y+)|(M+)|(d+)|(E+)|" +
478                     "(a+)|(B+)|(j+)|(h+)|(H+)|(m+)|(s+)|(S+)|(z+)|(O+)|(v+)"
479             )
480         private val TAG = this::class.qualifiedName
481 
482         /**
483          * Creates the a [DateTimeFormatterSkeletonOptions] object from a string.
484          *
485          * Although less discoverable than using the `Builder`, it is useful for serialization, and
486          * to implement the MessageFormat functionality.
487          *
488          * @param value the skeleton that specifies the fields to be formatted and their length.
489          * @return the formatting options to use with [androidx.core.i18n.DateTimeFormatter].
490          * @throws IllegalArgumentException if the [value] contains an unknown skeleton field.
491          * @throws RuntimeException library error (unknown skeleton field encountered).
492          */
493         @JvmStatic
fromStringnull494         public fun fromString(value: String): DateTimeFormatterSkeletonOptions {
495             val result = Builder()
496             if (value.isEmpty()) {
497                 return result.build()
498             }
499 
500             var validFields = false
501             val matcher = pattern.matcher(value)
502             while (matcher.find()) {
503                 validFields = true
504                 val skeletonField = matcher.group()
505                 when (skeletonField.firstOrNull()) {
506                     'G' -> result.setEra(Era.fromString(skeletonField))
507                     'y' -> result.setYear(Year.fromString(skeletonField))
508                     'M' -> result.setMonth(Month.fromString(skeletonField))
509                     'd' -> result.setDay(Day.fromString(skeletonField))
510                     'E' -> result.setWeekDay(WeekDay.fromString(skeletonField))
511                     'a',
512                     'B' -> result.setPeriod(Period.fromString(skeletonField))
513                     'j',
514                     'h',
515                     'H' -> result.setHour(Hour.fromString(skeletonField))
516                     'm' -> result.setMinute(Minute.fromString(skeletonField))
517                     's' -> result.setSecond(Second.fromString(skeletonField))
518                     'S' -> result.setFractionalSecond(FractionalSecond.fromString(skeletonField))
519                     'z',
520                     'O',
521                     'v' -> result.setTimezone(Timezone.fromString(skeletonField))
522                     else ->
523                         // This should not happen, the regexp should protect us.
524                         throw RuntimeException(
525                             "Unrecognized skeleton field '$skeletonField' in \"${value}\"."
526                         )
527                 }
528             }
529             if (!validFields) {
530                 throw IllegalArgumentException("Unrecognized skeleton field found in \"${value}\".")
531             }
532             return result.build()
533         }
534     }
535 
536     /**
537      * The `Builder` class used to construct a [DateTimeFormatterSkeletonOptions] in a way that is
538      * safe and discoverable.
539      */
540     public class Builder(
541         private var era: Era = Era.NONE,
542         private var year: Year = Year.NONE,
543         private var month: Month = Month.NONE,
544         private var day: Day = Day.NONE,
545         private var weekDay: WeekDay = WeekDay.NONE,
546         private var period: Period = Period.NONE,
547         private var hour: Hour = Hour.NONE,
548         private var minute: Minute = Minute.NONE,
549         private var second: Second = Second.NONE,
550         private var fractionalSecond: FractionalSecond = FractionalSecond.NONE,
551         private var timezone: Timezone = Timezone.NONE
552     ) {
553 
554         /**
555          * Set the era presence and length to use for formatting.
556          *
557          * @param era the era style to use.
558          */
setEranull559         public fun setEra(era: Era): Builder {
560             this.era = era
561             return this
562         }
563 
564         /**
565          * Set the year presence and length to use for formatting.
566          *
567          * @param year the era style to use.
568          */
setYearnull569         public fun setYear(year: Year): Builder {
570             this.year = year
571             return this
572         }
573 
574         /**
575          * Set the month presence and length to use for formatting.
576          *
577          * @param month the era style to use.
578          */
setMonthnull579         public fun setMonth(month: Month): Builder {
580             this.month = month
581             return this
582         }
583 
584         /**
585          * Set the day presence and length to use for formatting.
586          *
587          * @param day the era style to use.
588          */
setDaynull589         public fun setDay(day: Day): Builder {
590             this.day = day
591             return this
592         }
593 
594         /**
595          * Set the day of week presence and length to use for formatting.
596          *
597          * @param weekDay the era style to use.
598          */
setWeekDaynull599         public fun setWeekDay(weekDay: WeekDay): Builder {
600             this.weekDay = weekDay
601             return this
602         }
603 
604         /**
605          * Set the day period presence and length to use for formatting.
606          *
607          * @param period the era style to use.
608          */
setPeriodnull609         public fun setPeriod(period: Period): Builder {
610             this.period = period
611             return this
612         }
613 
614         /**
615          * Set the hour presence and length to use for formatting.
616          *
617          * @param hour the era style to use.
618          */
setHournull619         public fun setHour(hour: Hour): Builder {
620             this.hour = hour
621             return this
622         }
623 
624         /**
625          * Set the minute presence and length to use for formatting.
626          *
627          * @param minute the era style to use.
628          */
setMinutenull629         public fun setMinute(minute: Minute): Builder {
630             this.minute = minute
631             return this
632         }
633 
634         /**
635          * Set the second presence and length to use for formatting.
636          *
637          * @param second the era style to use.
638          */
setSecondnull639         public fun setSecond(second: Second): Builder {
640             this.second = second
641             return this
642         }
643 
644         /**
645          * Set the fractional second presence and length to use for formatting.
646          *
647          * @param fractionalSecond the era style to use.
648          */
setFractionalSecondnull649         public fun setFractionalSecond(fractionalSecond: FractionalSecond): Builder {
650             this.fractionalSecond = fractionalSecond
651             return this
652         }
653 
654         /**
655          * Set the timezone presence and length to use for formatting.
656          *
657          * @param timezone the era style to use.
658          */
setTimezonenull659         public fun setTimezone(timezone: Timezone): Builder {
660             this.timezone = timezone
661             return this
662         }
663 
664         /**
665          * Builds the immutable [DateTimeFormatterSkeletonOptions] to use with [DateTimeFormatter].
666          *
667          * return the [DateTimeFormatterSkeletonOptions] options.
668          */
buildnull669         public fun build(): DateTimeFormatterSkeletonOptions {
670             return DateTimeFormatterSkeletonOptions(
671                 era,
672                 year,
673                 month,
674                 day,
675                 weekDay,
676                 period,
677                 hour,
678                 minute,
679                 second,
680                 fractionalSecond,
681                 timezone
682             )
683         }
684     } // end of Builder class
685 }
686