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