DateTimeFormatterSkeletonOptions.kt

/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.core.i18n

import java.util.regex.Pattern

/**
 * This class helps one create skeleton options for [DateTimeFormatter]
 * in a safer and more discoverable manner than using raw strings.
 *
 * Skeletons are a flexible way to specify (in a locale independent manner)
 * how to format of a date / time.
 *
 * It can be used for example to specify that a formatted date should
 * contain a day-of-month, an abbreviated month name, and a year.
 *
 * It does not specify the order of the fields, or the separators,
 * those will depend on the locale.
 *
 * The result will be locale dependent: "Aug 17, 2022" for English U.S.,
 * "17 Aug 2022" for English - Great Britain, "2022年8月17日" for Japanese.
 *
 * Skeletons are based on the [Unicode Technical Standard #35](https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table),
 * but uses a builder to make things safer an more discoverable.
 *
 * You can still build these options from a string by using the
 * [DateTimeFormatterSkeletonOptions.fromString] method.
 */
class DateTimeFormatterSkeletonOptions internal constructor(
    private val era: Era,
    private val year: Year,
    private val month: Month,
    private val day: Day,
    private val weekDay: WeekDay,
    private val period: Period,
    private val hour: Hour,
    private val minute: Minute,
    private val second: Second,
    private val fractionalSecond: FractionalSecond,
    private val timezone: Timezone
) {

    /**********************************************
     * Date fields
     **********************************************/

    /** Era name. Era string for the date. */
    class Era private constructor(val value: String) {
        companion object {
            /** E.g. "Anno Domini". */
            @JvmField val WIDE = Era("GGGG")

            /** E.g. "AD". */
            @JvmField val ABBREVIATED = Era("G")

            /** E.g. "A". */
            @JvmField val NARROW = Era("GGGGG")

            /** Produces no output. */
            @JvmField val NONE = Era("")

            @JvmStatic
            fun fromString(value: String): Era {
                return when (value) {
                    "G", "GG", "GGG" -> ABBREVIATED
                    "GGGG" -> WIDE
                    "GGGGG" -> NARROW
                    else -> NONE
                }
            }
        }
    }

    /** Calendar year (numeric). */
    class Year private constructor(val value: String) {
        companion object {
            /** As many digits as needed to show the full value. E.g. "2021" or "2009". */
            @JvmField val NUMERIC = Year("y")

            /** The two low-order digits of the year, zero-padded as necessary. E.g. "21" or "09". */
            @JvmField val TWO_DIGITS = Year("yy")

            /** Produces no output. */
            @JvmField val NONE = Year("")

            @JvmStatic
           fun fromString(value: String): Year {
                return when (value) {
                    "y" -> NUMERIC
                    "yy" -> TWO_DIGITS
                    else -> NONE
                }
            }
        }
    }

    /** Month number/name. */
    class Month private constructor(val value: String) {
        companion object {
            /** e.g. "September". */
            @JvmField val WIDE = Month("MMMM")

            /** e.g. "Sep". */
            @JvmField val ABBREVIATED = Month("MMM")

            /** Might be soo short that it is confusing. E.g. "S". */
            @JvmField val NARROW = Month("MMMMM")

            /** E.g. "9". */
            @JvmField val NUMERIC = Month("M")

            /** Numeric: 2 digits, zero pad if needed. May not be good i18n. E.g. "09". */
            @JvmField val TWO_DIGITS = Month("MM")

            /** Produces no output. */
            @JvmField val NONE = Month("")

            @JvmStatic
           fun fromString(value: String): Month {
                return when (value) {
                    "M" -> NUMERIC
                    "MM" -> TWO_DIGITS
                    "MMM" -> ABBREVIATED
                    "MMMM" -> WIDE
                    "MMMMM" -> NARROW
                    else -> NONE
                }
            }
        }
    }

    /** Day of month (numeric). */
    class Day private constructor(val value: String) {
        companion object {
            /** As many digits as needed to show the full value. E.g. "1" or "17". */
            @JvmField val NUMERIC = Day("d")

            /** Two digits, zero pad if needed. E.g. "01" or "17". */
            @JvmField val TWO_DIGITS = Day("dd")

            /** Produces no output. */
            @JvmField val NONE = Day("")

           @JvmStatic
           fun fromString(value: String): Day {
                return when (value) {
                    "d" -> NUMERIC
                    "dd" -> TWO_DIGITS
                    else -> NONE
                }
            }
        }
    }

    /** Day of week name. */
    class WeekDay private constructor(val value: String) {
        companion object {
            /** E.g. "Tuesday". */
            @JvmField val WIDE = WeekDay("EEEE")

            /** E.g. "Tue". */
            @JvmField val ABBREVIATED = WeekDay("E")

            /** E.g. "Tu". */
            @JvmField val SHORT = WeekDay("EEEEEE")

            /** E.g. "T".
             * Two weekdays may have the same narrow style for some locales.
             * E.g. the narrow style for both "Tuesday" and "Thursday" is "T".
             */
            @JvmField val NARROW = WeekDay("EEEEE")

            /** Produces no output. */
            @JvmField val NONE = WeekDay("")

            @JvmStatic
            fun fromString(value: String): WeekDay {
                return when (value) {
                    "E", "EE", "EEE" -> ABBREVIATED
                    "EEEE" -> WIDE
                    "EEEEE" -> NARROW
                    "EEEEEE" -> SHORT
                    else -> NONE
                }
            }
        }
    }

    /**********************************************
     * Time fields
     **********************************************/

    /** The period of the day, if the hour is not 23h or 24h style. */
    class Period private constructor(val value: String) {
        companion object {
            /** E.g. "12 a.m.". */
            @JvmField val WIDE = Period("aaaa")

            /** E.g. "12 a.m.". */
            @JvmField val ABBREVIATED = Period("a")

            /** E.g. "12 a". */
            @JvmField val NARROW = Period("aaaaa")

            /** Flexible day periods. May be upper or lowercase depending on the locale and other options.
             * Often there is only one width that is customarily used.
             * E.g. "3:00 at night"
             */
            @JvmField val FLEXIBLE = Period("B")

            /** Produces no output. */
            @JvmField val NONE = Period("")

            @JvmStatic
           fun fromString(value: String): Period {
                return when (value) {
                    "a", "aa", "aaa" -> ABBREVIATED
                    "aaaa" -> WIDE
                    "aaaaa" -> NARROW
                    "B" -> FLEXIBLE
                    else -> NONE
                }
            }
        }
    }

    /** Hour (numeric). */
    class Hour private constructor(val value: String) {
        companion object {
            /** As many digits as needed to show the full value. Day period if used.
             * E.g. "8", "8 AM", "13", "1 PM".
             */
            @JvmField val NUMERIC = Hour("j")

            /** Two digits, zero pad if needed. DayPeriod if used.
             * Might be bad i18n. E.g. "08", "08 AM", "13", "01 PM".
             */
            @JvmField val TWO_DIGITS = Hour("jj")

            /** Bad i18n. As many digits as needed to show the full value. Day period added automatically.
             * E.g. "8 AM", "1 PM".
             */
            @JvmField val FORCE_12H_NUMERIC = Hour("h")

            /** Bad i18n. Two digits, zero pad if needed. Day period added automatically.
             * E.g. "08 AM", "01 PM".
             */
            @JvmField val FORCE_12H_TWO_DIGITS = Hour("hh")

            /** Bad i18n. As many digits as needed to show the full value. No day period.
             * E.g. "8", "13".
             */
            @JvmField val FORCE_24H_NUMERIC = Hour("H")

            /** Bad i18n. Two digits, zero pad if needed. No day period.
             * E.g. "08", "13".
             */
            @JvmField val FORCE_24H_TWO_DIGITS = Hour("HH")

            /** Produces no output. */
            @JvmField val NONE = Hour("")

            @JvmStatic
            fun fromString(value: String): Hour {
                return when (value) {
                    "j" -> NUMERIC
                    "jj" -> TWO_DIGITS
                    "h" -> FORCE_12H_NUMERIC
                    "hh" -> FORCE_12H_TWO_DIGITS
                    "H" -> FORCE_24H_NUMERIC
                    "HH" -> FORCE_24H_TWO_DIGITS
                    else -> NONE
                }
            }
        }
    }

    /** Minute (numeric). Truncated, not rounded. */
    class Minute private constructor(val value: String) {
        companion object {
            /** As many digits as needed to show the full value. E.g. "8", "59" */
            @JvmField val NUMERIC = Minute("m")

            /** Two digits, zero pad if needed. E.g. "08", "59" */
            @JvmField val TWO_DIGITS = Minute("mm")

            /** Produces no output. */
            @JvmField val NONE = Minute("")

            @JvmStatic
            fun fromString(value: String): Minute {
                return when (value) {
                    "m" -> NUMERIC
                    "mm" -> TWO_DIGITS
                    else -> NONE
                }
            }
        }
    }

    /** Second (numeric). Truncated, not rounded. */
    class Second private constructor(val value: String) {
        companion object {
            /** As many digits as needed to show the full value. E.g. "8", "59". */
            @JvmField val NUMERIC = Second("s")

            /** Two digits, zero pad if needed. E.g. "08", "59". */
            @JvmField val TWO_DIGITS = Second("ss")

            /** Produces no output. */
            @JvmField val NONE = Second("")

            @JvmStatic
            fun fromString(value: String): Second {
                return when (value) {
                    "s" -> NUMERIC
                    "ss" -> TWO_DIGITS
                    else -> NONE
                }
            }
        }
    }

    /** Fractional Second (numeric). Truncates, like other numeric time fields,
     * but in this case to the number of digits specified by the field length.
     */
    class FractionalSecond private constructor(val value: String) {
        companion object {
            /** Fractional part represented as 3 digits. E.g. "12.345". */
            @JvmField val NUMERIC_3_DIGITS = FractionalSecond("SSS")

            /** Fractional part represented as 2 digits. E.g. "12.34". */
            @JvmField val NUMERIC_2_DIGITS = FractionalSecond("SS")

            /** Fractional part represented as 1 digit. E.g. "12.3". */
            @JvmField val NUMERIC_1_DIGIT = FractionalSecond("S")

            /** Fractional part dropped. Produces no output. E.g. "12" (seconds, without fractions). */
            @JvmField val NONE = FractionalSecond("")

            @JvmStatic
            fun fromString(value: String):
                    FractionalSecond {

                return when (value) {
                    "S" -> NUMERIC_1_DIGIT
                    "SS" -> NUMERIC_2_DIGITS
                    "SSS" -> NUMERIC_3_DIGITS
                    else -> NONE
                }
            }
        }
    }

    /** The localized representation of the time zone name. */
    class Timezone private constructor(val value: String) {
        companion object {
            /** Short localized form. E.g. "PST", "GMT-8". */
            @JvmField val SHORT = Timezone("z")

            /** Long localized form. E.g. "Pacific Standard Time", "Nordamerikanische Westküsten-Normalzeit". */
            @JvmField val LONG = Timezone("zzzz")

            /** Short localized GMT format. E.g. "GMT-8". */
            @JvmField val SHORT_OFFSET = Timezone("O")

            /** Long localized GMT format. E.g. "GMT-0800". */
            @JvmField val LONG_OFFSET = Timezone("OOOO")

            /** Short generic non-location format. E.g. "PT", "Los Angeles Zeit". */
            @JvmField val SHORT_GENERIC = Timezone("v")

            /** Long generic non-location format. E.g. "Pacific Time", "Nordamerikanische Westküstenzeit". */
            @JvmField val LONG_GENERIC = Timezone("vvvv")

            /** Produces no output. */
            @JvmField val NONE = Timezone("")

            @JvmStatic
           fun fromString(value: String): Timezone {
                return when (value) {
                    "z", "zz", "zzz" -> SHORT
                    "zzzz" -> LONG
                    "O" -> SHORT_OFFSET
                    "OOOO" -> LONG_OFFSET
                    "v" -> SHORT_GENERIC
                    "vvvv" -> LONG_GENERIC
                    else -> NONE
                }
            }
        }
    }

    /** Returns the era option. */
    fun getEra(): Era = era
    /** Returns the year option. */
    fun getYear(): Year = year
    /** Returns the month option. */
    fun getMonth(): Month = month
    /** Returns the day option. */
    fun getDay(): Day = day
    /** Returns the day of week option. */
    fun getWeekDay(): WeekDay = weekDay
    /** Returns the day period option. */
    fun getPeriod(): Period = period
    /** Returns the hour option. */
    fun getHour(): Hour = hour
    /** Returns the minutes option. */
    fun getMinute(): Minute = minute
    /** Returns the seconds option. */
    fun getSecond(): Second = second
    /** Returns the fractional second option. */
    fun getFractionalSecond(): FractionalSecond = fractionalSecond
    /** Returns the timezone option. */
    fun getTimezone(): Timezone = timezone

    override fun toString(): String {
        return era.value +
                year.value +
                month.value +
                weekDay.value +
                day.value +
                period.value +
                hour.value +
                minute.value +
                second.value +
                fractionalSecond.value +
                timezone.value
    }

    companion object {
        // WARNING: if you change this regexp also update the switch in [fromString]
        private val pattern = Pattern.compile("(G+)|(y+)|(M+)|(d+)|(E+)|" +
                "(a+)|(B+)|(j+)|(h+)|(H+)|(m+)|(s+)|(S+)|(z+)|(O+)|(v+)")
        private val TAG = this::class.qualifiedName

        /**
         * Creates the a [DateTimeFormatterSkeletonOptions] object from a string.
         *
         * Although less discoverable than using the `Builder`, it is useful for serialization,
         * and to implement the MessageFormat functionality.
         *
         * @param value the skeleton that specifies the fields to be formatted and their length.
         * @return the formatting options to use with [androidx.core.i18n.DateTimeFormatter].
         *
         * @throws IllegalArgumentException if the [value] contains an unknown skeleton field.
         * @throws RuntimeException library error (unknown skeleton field encountered).
         */
        @JvmStatic
        fun fromString(value: String): DateTimeFormatterSkeletonOptions {
            val result = Builder()
            if (value.isEmpty()) {
                return result.build()
            }

            var validFields = false
            val matcher = pattern.matcher(value)
            while (matcher.find()) {
                validFields = true
                val skeletonField = matcher.group()
                when (skeletonField.firstOrNull()) {
                    'G' -> result.setEra(Era.fromString(skeletonField))
                    'y' -> result.setYear(Year.fromString(skeletonField))
                    'M' -> result.setMonth(Month.fromString(skeletonField))
                    'd' -> result.setDay(Day.fromString(skeletonField))
                    'E' -> result.setWeekDay(WeekDay.fromString(skeletonField))
                    'a', 'B' -> result.setPeriod(Period.fromString(skeletonField))
                    'j', 'h', 'H' -> result.setHour(Hour.fromString(skeletonField))
                    'm' -> result.setMinute(Minute.fromString(skeletonField))
                    's' -> result.setSecond(Second.fromString(skeletonField))
                    'S' -> result.setFractionalSecond(FractionalSecond.fromString(skeletonField))
                    'z', 'O', 'v' -> result.setTimezone(Timezone.fromString(skeletonField))
                    else ->
                        // This should not happen, the regexp should protect us.
                        throw RuntimeException(
                            "Unrecognized skeleton field '$skeletonField' in \"${value}\".")
                }
            }
            if (!validFields) {
                throw IllegalArgumentException(
                    "Unrecognized skeleton field found in \"${value}\".")
            }
            return result.build()
        }
    }

    /**
     * The `Builder` class used to construct a [DateTimeFormatterSkeletonOptions] in a way
     * that is safe and discoverable.
     */
    class Builder(
        private var era: Era = Era.NONE,
        private var year: Year = Year.NONE,
        private var month: Month = Month.NONE,
        private var day: Day = Day.NONE,
        private var weekDay: WeekDay = WeekDay.NONE,
        private var period: Period = Period.NONE,
        private var hour: Hour = Hour.NONE,
        private var minute: Minute = Minute.NONE,
        private var second: Second = Second.NONE,
        private var fractionalSecond: FractionalSecond = FractionalSecond.NONE,
        private var timezone: Timezone = Timezone.NONE
    ) {

        /** Set the era presence and length to use for formatting.
         * @param era the era style to use.
         */
        fun setEra(era: Era): Builder {
            this.era = era
            return this
        }

        /** Set the year presence and length to use for formatting.
         * @param year the era style to use.
         */
        fun setYear(year: Year): Builder {
            this.year = year
            return this
        }

        /** Set the month presence and length to use for formatting.
         * @param month the era style to use.
         */
        fun setMonth(month: Month): Builder {
            this.month = month
            return this
        }

        /** Set the day presence and length to use for formatting.
         * @param day the era style to use.
         */
        fun setDay(day: Day): Builder {
            this.day = day
            return this
        }

        /** Set the day of week presence and length to use for formatting.
         * @param weekDay the era style to use.
         */
        fun setWeekDay(weekDay: WeekDay): Builder {
            this.weekDay = weekDay
            return this
        }

        /** Set the day period presence and length to use for formatting.
         * @param period the era style to use.
         */
        fun setPeriod(period: Period): Builder {
            this.period = period
            return this
        }

        /** Set the hour presence and length to use for formatting.
         * @param hour the era style to use.
         */
        fun setHour(hour: Hour): Builder {
            this.hour = hour
            return this
        }

        /** Set the minute presence and length to use for formatting.
         * @param minute the era style to use.
         */
        fun setMinute(minute: Minute): Builder {
            this.minute = minute
            return this
        }

        /** Set the second presence and length to use for formatting.
         * @param second the era style to use.
         */
        fun setSecond(second: Second): Builder {
            this.second = second
            return this
        }

        /** Set the fractional second presence and length to use for formatting.
         * @param fractionalSecond the era style to use.
         */
        fun setFractionalSecond(
            fractionalSecond: FractionalSecond
        ): Builder {
            this.fractionalSecond = fractionalSecond
            return this
        }

        /** Set the timezone presence and length to use for formatting.
         * @param timezone the era style to use.
         */
        fun setTimezone(timezone: Timezone): Builder {
            this.timezone = timezone
            return this
        }

        /** Builds the immutable [DateTimeFormatterSkeletonOptions] to use with [DateTimeFormatter].
         *
         * return the [DateTimeFormatterSkeletonOptions] options.
         */
        fun build(): DateTimeFormatterSkeletonOptions {
            return DateTimeFormatterSkeletonOptions(era, year, month, day, weekDay,
                period, hour, minute, second, fractionalSecond, timezone)
        }
    } // end of Builder class
}