Dates.kt

  1. package com.hexagonkt.core

  2. import java.time.*
  3. import java.util.Date

  4. private const val DATE_OFFSET: Long = 1_000_000_000L
  5. private const val YEAR_OFFSET: Int = 10_000
  6. private const val MONTH_OFFSET: Int = 100
  7. private const val HOUR_OFFSET: Int = 10_000_000
  8. private const val MINUTE_OFFSET: Int = 100_000
  9. private const val SECOND_OFFSET: Int = 1_000
  10. private const val NANO_OFFSET: Int = 1_000_000
  11. private const val DAYS_PER_MONTH: Double = 30.4375

  12. /** GMT zone ID. */
  13. val GMT_ZONE: ZoneId by lazy { ZoneId.of("GMT") }

  14. /**
  15.  * Convert a date time to a number with the following format: `YYYYMMDDHHmmss`.
  16.  *
  17.  * @receiver Date to be converted to a number.
  18.  * @return Numeric representation of the given date.
  19.  */
  20. fun LocalDateTime.toNumber(): Long =
  21.     (this.toLocalDate().toNumber() * DATE_OFFSET) + this.toLocalTime().toNumber()

  22. /**
  23.  * Convert a date to an integer with the following format: `YYYYMMDD`.
  24.  *
  25.  * @receiver Date to be converted to a number.
  26.  * @return Numeric representation of the given date.
  27.  */
  28. fun LocalDate.toNumber(): Int =
  29.     (this.year * YEAR_OFFSET) +
  30.     (this.monthValue * MONTH_OFFSET) +
  31.     this.dayOfMonth

  32. /**
  33.  * Convert a time to an integer with the following format: `HHmmssSSS`.
  34.  *
  35.  * @receiver Time to be converted to a number.
  36.  * @return Numeric representation of the given time.
  37.  */
  38. fun LocalTime.toNumber(): Int =
  39.     (this.hour * HOUR_OFFSET) +
  40.     (this.minute * MINUTE_OFFSET) +
  41.     (this.second * SECOND_OFFSET) +
  42.     (this.nano / NANO_OFFSET) // Nanos to millis

  43. /**
  44.  * Return the date time in a given time zone for a local date time.
  45.  *
  46.  * @receiver Local date time to be moved to another time zone.
  47.  * @param zoneId Id of the target zone of the passed local date time.
  48.  * @return Received date time at the given [zoneId].
  49.  */
  50. fun LocalDateTime.withZone(zoneId: ZoneId = Jvm.timeZone.toZoneId()): ZonedDateTime =
  51.     ZonedDateTime.of(this, zoneId)

  52. /**
  53.  * Parse a date time from a formatted number with this format: `YYYYMMDDHHmmss`.
  54.  *
  55.  * @receiver Number to be converted to a date time.
  56.  * @return Local date time representation of the given number.
  57.  */
  58. fun Long.toLocalDateTime(): LocalDateTime {
  59.     require(this >= 0) { "Number representing timestamp must be positive (format: YYYYMMDDHHmmss)" }
  60.     return (this / DATE_OFFSET)
  61.         .toInt()
  62.         .toLocalDate()
  63.         .atTime((this % DATE_OFFSET).toInt().toLocalTime())
  64. }

  65. /**
  66.  * Parse a date from a formatted integer with this format: `YYYYMMDD`.
  67.  *
  68.  * @receiver Number to be converted to a date.
  69.  * @return Local date representation of the given number.
  70.  */
  71. fun Int.toLocalDate(): LocalDate {
  72.     require(this >= 0) { "Number representing date must be positive (format: YYYYMMDD)" }
  73.     return LocalDate.of(
  74.         this / YEAR_OFFSET,
  75.         (this % YEAR_OFFSET) / MONTH_OFFSET,
  76.         this % MONTH_OFFSET
  77.     )
  78. }

  79. /**
  80.  * Parse a time from a formatted integer with this format: `HHmmssSSS`.
  81.  *
  82.  * @receiver Number to be converted to a time.
  83.  * @return Local time representation of the given number.
  84.  */
  85. fun Int.toLocalTime(): LocalTime {
  86.     require(this >= 0) { "Number representing time must be positive (format: HHmmssSSS)" }
  87.     return LocalTime.of(
  88.         (this / HOUR_OFFSET),
  89.         ((this % HOUR_OFFSET) / MINUTE_OFFSET),
  90.         ((this % MINUTE_OFFSET) / SECOND_OFFSET),
  91.         ((this % SECOND_OFFSET) * NANO_OFFSET) // Millis to nanos
  92.     )
  93. }

  94. /**
  95.  * Convert a zoned date time to a date.
  96.  *
  97.  * @receiver Zoned date time to be converted to a date.
  98.  * @return Date representation of the given zoned date time.
  99.  */
  100. fun ZonedDateTime.toDate(): Date =
  101.     Date.from(this.toInstant())

  102. /**
  103.  * Convert a local date time to a date.
  104.  *
  105.  * @receiver Local date time to be converted to a date.
  106.  * @return Date representation of the given local date time.
  107.  */
  108. fun LocalDateTime.toDate(): Date =
  109.     this.atZone(Jvm.timeZone.toZoneId()).toDate()

  110. /**
  111.  * Convert a local date to a date.
  112.  *
  113.  * @receiver Local date to be converted to a date.
  114.  * @return Date representation of the given local date.
  115.  */
  116. fun LocalDate.toDate(): Date =
  117.     this.atStartOfDay(Jvm.timeZone.toZoneId()).toDate()

  118. /**
  119.  * Convert a date to a local date time.
  120.  *
  121.  * @receiver Date to be converted to a local date time.
  122.  * @return Local date time representation of the given date.
  123.  */
  124. fun Date.toLocalDateTime(): LocalDateTime =
  125.     LocalDateTime.ofInstant(Instant.ofEpochMilli(this.time), ZoneId.systemDefault())

  126. /**
  127.  * Convert a date to a local date.
  128.  *
  129.  * @receiver Date to be converted to a local date.
  130.  * @return Local date representation of the given date.
  131.  */
  132. fun Date.toLocalDate(): LocalDate =
  133.     this.toLocalDateTime().toLocalDate()

  134. /**
  135.  * Calculate the aproximate number of days comprised in a time period.
  136.  *
  137.  * @receiver Period from which calculate the number of days.
  138.  * @return Aproximate number of days of the period.
  139.  */
  140. fun Period.toTotalDays(): Double =
  141.     (toTotalMonths() * DAYS_PER_MONTH) + days

  142. /**
  143.  * Parse a time period allowing a more relaxed format: with spaces or commas, lowercase characters
  144.  * and not forcing the text to start with 'P'.
  145.  *
  146.  * @param text Text to be parsed to a time period.
  147.  * @return Time period parsed from the supplied text.
  148.  */
  149. fun parsePeriod(text: String): Period =
  150.     Period.parse(formatDuration(text))

  151. /**
  152.  * Parse a time duration allowing a more relaxed format: with spaces or commas, lowercase characters
  153.  * and not forcing the text to start with 'P', however, the 'T' is still mandatory to separate date
  154.  * and time durations.
  155.  *
  156.  * @param text Text to be parsed to a time duration.
  157.  * @return Time duration parsed from the supplied text.
  158.  */
  159. fun parseDuration(text: String): Duration =
  160.     Duration.parse(formatDuration(text))

  161. private fun formatDuration(text: String): String =
  162.     text
  163.         .replace(",", "")
  164.         .replace(" ", "")
  165.         .uppercase()
  166.         .let { if (it.startsWith("P")) it else "P$it" }

  167. /**
  168.  * Parse a local date allowing only to specify the year or the year and the month. Missing month and
  169.  * day will be defaulted to january and one respectively.
  170.  *
  171.  * @param text Text to be parsed to a local date.
  172.  * @return Local date parsed from the supplied text.
  173.  */
  174. fun parseLocalDate(text: String): LocalDate =
  175.     when (text.length) {
  176.         4 -> Year.parse(text).atMonth(1).atDay(1)
  177.         7 -> YearMonth.parse(text).atDay(1)
  178.         else -> LocalDate.parse(text)
  179.     }