All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ai.platon.pulsar.common.DateTimes.kt Maven / Gradle / Ivy

There is a newer version: 2.1.0
Show newest version
package ai.platon.pulsar.common

import org.apache.commons.lang3.StringUtils
import org.apache.http.client.utils.DateUtils
import java.text.SimpleDateFormat
import java.time.*
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.time.temporal.TemporalAccessor
import java.util.*

data class JvmTimedValue(val value: T, val duration: Duration)

inline fun  measureTimedValueJvm(block: () -> T): JvmTimedValue {
    val startTime = Instant.now()
    val value = block()
    val elapsedTime = Duration.between(startTime, Instant.now())
    return JvmTimedValue(value, elapsedTime)
}

inline fun  measureTimeJvm(block: () -> Unit): Duration {
    val startTime = Instant.now()
    block()
    return Duration.between(startTime, Instant.now())
}

fun ChronoUnit.shortName(): String = when (this) {
    ChronoUnit.NANOS -> "ns"
    ChronoUnit.MICROS -> "us"
    ChronoUnit.MILLIS -> "ms"
    ChronoUnit.SECONDS -> "s"
    ChronoUnit.MINUTES -> "m"
    ChronoUnit.HOURS -> "h"
    ChronoUnit.DAYS -> "d"
    else -> this.name
}

object DateTimes {
    private val logger = getLogger(DateTimes::class)

    val PATH_SAFE_FORMAT_1 = SimpleDateFormat("MMdd")
    val PATH_SAFE_FORMAT_2 = SimpleDateFormat("MMdd.HH")
    val PATH_SAFE_FORMAT_3 = SimpleDateFormat("MMdd.HHmm")
    val PATH_SAFE_FORMAT_4 = SimpleDateFormat("MMdd.HHmmss")

    // inaccurate date time
    const val HOURS_PER_DAY = 24L
    const val HOURS_PER_MONTH = HOURS_PER_DAY * 30
    const val HOURS_PER_YEAR = HOURS_PER_DAY * 365

    const val MILLIS_PER_SECOND = 1000L
    const val MILLIS_PER_MINUTE = 60 * MILLIS_PER_SECOND
    const val MILLIS_PER_HOUR = 60 * MILLIS_PER_MINUTE
    const val MILLIS_PER_DAY = 24 * MILLIS_PER_HOUR

    const val MINUTES_PER_HOUR = 60L

    const val SECONDS_PER_MINUTE = 60L
    const val SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR
    const val SECONDS_PER_DAY = SECONDS_PER_HOUR * HOURS_PER_DAY

    val ONE_YEAR_LATER: Instant = Instant.now() + Duration.ofDays(365)

    const val DATE_REGEX = "\\d{4}-\\d{2}-\\d{2}"
    const val DATE_TIME_REGEX = "${DATE_REGEX}T\\d{2}:.+"
    const val SIMPLE_DATE_TIME_REGEX = "$DATE_REGEX\\s+\\d{2}:.+"

    const val PULSAR_ZONE_ID = "pulsar.zone.id"

    /**
     * The default zone id
     * */
    var zoneId = ZoneId.of(System.getProperty(PULSAR_ZONE_ID, ZoneId.systemDefault().id))

    /**
     * The default zone offset, it must be consistent with zoneId
     *
     * There is no one-to-one mapping. A ZoneId defines a geographic extent in which a set of different ZoneOffsets is used over time. If the timezone uses daylight saving time, its ZoneOffset will be different between summer and winter.
     * Furthermore, the daylight saving time rules may have changed over time, so the ZoneOffset could be different for e.g. 13/10/2015 compared to 13/10/1980.
     * So you can only find the ZoneOffset for a ZoneId on a particular Instant.
     * See also [Tz_database](https://en.wikipedia.org/wiki/Tz_database)
     * */
    val zoneOffset get() = zoneId.rules.getOffset(Instant.now())

    /**
     * The time to start the program
     */
    val startTime = Instant.now()

    /**
     * We foresee that 2200 will be the end of the world, care about nothing after the doom
     * */
    val doomsday = Instant.parse("2200-01-01T00:00:00Z")

    val midnight get() = LocalDateTime.now().truncatedTo(ChronoUnit.DAYS)
    val startOfHour get() = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS)

    val elapsed get() = Duration.between(startTime, Instant.now())
    val elapsedToday get() = Duration.between(midnight, LocalDateTime.now())
    val elapsedThisHour get() = Duration.between(startOfHour, LocalDateTime.now())

    fun zoneIdOrDefault(name: String): ZoneId {
        return if (name in ZoneId.getAvailableZoneIds()) {
            ZoneId.of(name)
        } else {
            zoneId
        }
    }

    fun format(time: Long): String {
        return DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(zoneId).format(Instant.ofEpochMilli(time))
    }

    fun format(time: Instant): String {
        return DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(zoneId).format(time)
    }

    fun format(time: Instant, format: String): String {
        return DateTimeFormatter.ofPattern(format).withZone(zoneId).format(time)
    }

    fun format(localTime: LocalDateTime): String {
        return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(localTime)
    }

    fun format(localTime: LocalDateTime, format: String): String {
        return DateTimeFormatter.ofPattern(format).format(localTime)
    }

    fun format(localTime: OffsetDateTime, format: String): String {
        return DateTimeFormatter.ofPattern(format).format(localTime)
    }

    fun format(epochMilli: Long, format: String): String {
        return format(Instant.ofEpochMilli(epochMilli), format)
    }

    fun formatNow(format: String): String {
        return format(Instant.now(), format)
    }

    @JvmOverloads
    fun readableDuration(duration: Duration, truncatedToUnit: ChronoUnit = ChronoUnit.SECONDS): String {
        return StringUtils.removeStart(duration.truncatedTo(truncatedToUnit).toString(), "PT")
            .lowercase(Locale.getDefault())
    }

    fun readableDuration(duration: String): String {
        return StringUtils.removeStart(duration, "PT").lowercase(Locale.getDefault())
    }

    fun isoInstantFormat(time: Long): String {
        return DateTimeFormatter.ISO_INSTANT.format(Date(time).toInstant())
    }

    fun isoInstantFormat(date: Date): String {
        return DateTimeFormatter.ISO_INSTANT.format(date.toInstant())
    }

    @JvmStatic
    fun isoInstantFormat(time: Instant): String {
        return DateTimeFormatter.ISO_INSTANT.format(time)
    }

    fun now(format: String): String {
        return format(System.currentTimeMillis(), format)
    }

    @JvmStatic
    fun now(): String {
        return format(LocalDateTime.now())
    }

    fun toLocalDate(instant: Instant): LocalDate {
        return instant.atZone(zoneId).toLocalDate()
    }

    fun toLocalDateTime(instant: Instant): LocalDateTime {
        return instant.atZone(zoneId).toLocalDateTime()
    }

    fun toOffsetDateTime(instant: Instant): OffsetDateTime {
        return instant.atZone(zoneId).toOffsetDateTime()
    }

    fun startOfHour(): Instant {
        return Instant.now().truncatedTo(ChronoUnit.HOURS)
    }

    fun endOfHour(): Instant {
        return Instant.now().truncatedTo(ChronoUnit.HOURS).plus(Duration.ofHours(1))
    }

    fun startOfDay(): Instant {
        return LocalDate.now().atStartOfDay().toInstant(zoneOffset)
    }

    fun endOfDay(): Instant {
        return LocalDate.now().atStartOfDay().toInstant(zoneOffset).plus(Duration.ofHours(24))
    }

    fun timePointOfDay(hour: Int, minute: Int = 0, second: Int = 0): Instant {
        return LocalDate.now().atTime(hour, minute, second).toInstant(zoneOffset)
    }

    fun dayOfWeek() = dayOfWeek(Instant.now())

    fun dayOfWeek(instant: Instant): DayOfWeek {
        return instant.atZone(zoneId).dayOfWeek
    }

    fun dayOfMonth() = dayOfMonth(Instant.now())

    fun dayOfMonth(instant: Instant): Int {
        return instant.atZone(zoneId).dayOfMonth
    }

    fun elapsedTime(): Duration {
        return elapsedTime(startTime, Instant.now())
    }

    fun elapsedTime(start: Long): Duration {
        return elapsedTime(Instant.ofEpochMilli(start), Instant.now())
    }

    /**
     * The imprecise elapsed time in seconds, at least 1 second
     * */
    fun elapsedSeconds(): Long {
        return elapsedTime().seconds.coerceAtLeast(1)
    }

    /**
     * Calculate the elapsed time between two times specified in milliseconds.
     */
    @JvmOverloads
    fun elapsedTime(start: Instant, end: Instant = Instant.now()): Duration = Duration.between(start, end)

    fun isExpired(start: Instant, expiry: Duration) = start + expiry < Instant.now()

    fun isNotExpired(start: Instant, expiry: Duration) = !isExpired(start, expiry)

    /**
     * RFC 2616 defines three different date formats that a conforming client must understand.
     */
    @JvmStatic
    fun parseHttpDateTime(text: String, defaultValue: Instant): Instant {
        return try {
            val d = DateUtils.parseDate(text)
            d.toInstant()
        } catch (e: Throwable) {
            defaultValue
        }
    }

    fun formatHttpDateTime(time: Long): String {
        return DateUtils.formatDate(Date(time))
    }

    @JvmStatic
    fun formatHttpDateTime(time: Instant): String {
        return DateUtils.formatDate(Date.from(time))
    }

    @JvmOverloads
    @JvmStatic
    fun parseInstant(text: String, defaultValue: Instant = Instant.EPOCH): Instant {
        try {
            // equals to Instant.parse()
            return DateTimeFormatter.ISO_INSTANT.parse(text) { temporal: TemporalAccessor? -> Instant.from(temporal) }
        } catch (ignored: Throwable) {
        }
        return defaultValue
    }

    /**
     * Accept the following format:
     * 1. yyyy-MM-dd[ HH[:mm[:ss]]]
     * 2. ISO_INSTANT, or yyyy-MM-ddTHH:mm:ssZ
     * */
    @JvmOverloads
    @JvmStatic
    fun parseBestInstant(text: String, defaultValue: Instant = Instant.EPOCH): Instant {
        return parseBestInstantOrNull(text) ?: defaultValue
    }

    /**
     * Accept the following format:
     * 1. yyyy-MM-dd[ HH[:mm[:ss]]]
     * 2. ISO_INSTANT, or yyyy-MM-ddTHH:mm:ssZ
     * */
    @JvmStatic
    fun parseBestInstantOrNull(text: String): Instant? {
        try {
            return when {
                text.isBlank() -> null
                text.matches("${DATE_REGEX}T\\d{2}.+Z".toRegex()) -> {
                    Instant.parse(text)
                }
                text.matches(SIMPLE_DATE_TIME_REGEX.toRegex()) -> {
                    val pattern = "yyyy-MM-dd HH[:mm][:ss]"
                    DateTimeFormatter.ofPattern(pattern)
                        .parse(text) { LocalDateTime.from(it) }
                        .atZone(zoneId).toInstant()
                }
                text.matches(DATE_TIME_REGEX.toRegex()) -> {
                    val pattern = "yyyy-MM-dd'T'HH[:mm][:ss]"
                    DateTimeFormatter.ofPattern(pattern)
                        .parse(text) { LocalDateTime.from(it) }
                        .atZone(zoneId).toInstant()
                }
                text.matches(DATE_REGEX.toRegex()) -> {
                    val pattern = "yyyy-MM-dd"
                    DateTimeFormatter.ofPattern(pattern)
                        .parse(text) { LocalDate.from(it) }
                        .atStartOfDay().atZone(zoneId).toInstant()
                }
                else -> null
            }
        } catch (e: Throwable) {
            logger.warn("Failed to parse $text | {}", e)
        }

        return null
    }

    @JvmStatic
    fun parseDuration(text: String, defaultValue: Duration): Duration {
        return try {
            Duration.parse(text)
        } catch (ignored: Throwable) {
            defaultValue
        }
    }

    @JvmStatic
    fun parseDurationOrNull(text: String): Duration? {
        return try {
            Duration.parse(text)
        } catch (ignored: Throwable) {
            null
        }
    }

    fun isDuration(text: String) = parseDurationOrNull(text) != null

    @JvmStatic
    fun constructTimeHistory(timeHistory: String?, fetchTime: Instant, maxRecords: Int): String {
        var history = timeHistory
        val dateStr = isoInstantFormat(fetchTime)
        if (history == null) {
            history = dateStr
        } else {
            val fetchTimes = history.split(",").toTypedArray()
            if (fetchTimes.size > maxRecords) {
                val firstFetchTime = fetchTimes[0]
                val start = fetchTimes.size - maxRecords
                val end = fetchTimes.size
                history = firstFetchTime + ',' + StringUtils.join(fetchTimes, ',', start, end)
            }
            history += ","
            history += dateStr
        }
        return history
    }

    @JvmStatic
    fun isDaysBefore(dateTime: OffsetDateTime, days: Int): Boolean {
        return DateTimeDetector.CURRENT_DATE_EPOCH_DAYS - dateTime.toLocalDate().toEpochDay() > days
    }
}

fun Duration.readable() = DateTimes.readableDuration(this)




© 2015 - 2025 Weber Informatics LLC | Privacy Policy