-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathDateTime.kt
More file actions
273 lines (229 loc) · 9.91 KB
/
DateTime.kt
File metadata and controls
273 lines (229 loc) · 9.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
package to.bitkit.ext
import android.icu.text.DateFormat
import android.icu.text.DisplayContext
import android.icu.text.NumberFormat
import android.icu.text.RelativeDateTimeFormatter
import android.icu.text.RelativeDateTimeFormatter.AbsoluteUnit
import android.icu.text.RelativeDateTimeFormatter.Direction
import android.icu.text.RelativeDateTimeFormatter.RelativeUnit
import android.icu.util.ULocale
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.number
import kotlinx.datetime.toJavaLocalDate
import kotlinx.datetime.toJavaMonth
import kotlinx.datetime.toKotlinLocalDate
import kotlinx.datetime.toLocalDateTime
import java.text.SimpleDateFormat
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.util.Calendar
import java.util.Date
import java.util.Locale
import kotlin.time.Clock
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.ExperimentalTime
import kotlin.time.Instant as KInstant
@OptIn(ExperimentalTime::class)
fun nowMillis(clock: Clock = Clock.System): Long = clock.now().toEpochMilliseconds()
@OptIn(ExperimentalTime::class)
fun Clock.nowMs(): Long = now().toEpochMilliseconds()
fun nowTimestamp(): Instant = Instant.now().truncatedTo(ChronoUnit.SECONDS)
fun Instant.formatted(pattern: String = DatePattern.DATE_TIME): String {
val dateTime = LocalDateTime.ofInstant(this, ZoneId.systemDefault())
val formatter = DateTimeFormatter.ofPattern(pattern)
return dateTime.format(formatter)
}
fun ULong?.formatToString(pattern: String = DatePattern.DATE_TIME): String? {
return this?.let { Instant.ofEpochSecond(toLong()).formatted(pattern) }
}
fun Long.toTimeUTC(): String {
val instant = Instant.ofEpochMilli(this)
val dateTime = LocalDateTime.ofInstant(instant, ZoneId.of("UTC"))
return dateTime.format(DateTimeFormatter.ofPattern("HH:mm:ss"))
}
fun Long.toDateUTC(): String {
val instant = Instant.ofEpochMilli(this)
val dateTime = LocalDateTime.ofInstant(instant, ZoneId.of("UTC"))
return dateTime.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))
}
fun Long.toLocalizedTimestamp(locale: Locale = Locale.US): String {
val date = Date(this)
val formatter = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT, ULocale.forLocale(locale))
?: return SimpleDateFormat("MMMM d, yyyy 'at' h:mm a", locale).format(date)
return formatter.format(date)
}
@Suppress("LongMethod")
@OptIn(ExperimentalTime::class)
fun Long.toRelativeTimeString(
locale: Locale = Locale.getDefault(),
clock: Clock = Clock.System,
style: RelativeDateTimeFormatter.Style = RelativeDateTimeFormatter.Style.LONG,
capitalizationContext: DisplayContext = DisplayContext.CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE,
): String {
val now = nowMillis(clock)
val diffMillis = now - this
val uLocale = ULocale.forLocale(locale)
val numberFormat = NumberFormat.getNumberInstance(uLocale)?.apply { maximumFractionDigits = 0 }
val formatter = RelativeDateTimeFormatter.getInstance(
uLocale,
numberFormat,
style,
capitalizationContext,
) ?: return toLocalizedTimestamp(locale)
val seconds = diffMillis / Factor.MILLIS_TO_SECONDS
val minutes = seconds / Factor.SECONDS_TO_MINUTES
val hours = minutes / Factor.MINUTES_TO_HOURS
val days = hours / Factor.HOURS_TO_DAYS
val weeks = days / Factor.DAYS_TO_WEEKS
val months = days / Factor.DAYS_TO_MONTHS
val years = days / Factor.DAYS_TO_YEARS
return when {
seconds < Threshold.SECONDS -> formatter.format(Direction.PLAIN, AbsoluteUnit.NOW)
minutes < Threshold.MINUTES -> formatter.format(minutes, Direction.LAST, RelativeUnit.MINUTES)
hours < Threshold.HOURS -> formatter.format(hours, Direction.LAST, RelativeUnit.HOURS)
days < Threshold.YESTERDAY -> formatter.format(Direction.LAST, AbsoluteUnit.DAY)
days < Threshold.DAYS -> formatter.format(days, Direction.LAST, RelativeUnit.DAYS)
weeks < Threshold.WEEKS -> formatter.format(weeks, Direction.LAST, RelativeUnit.WEEKS)
months < Threshold.MONTHS -> formatter.format(months, Direction.LAST, RelativeUnit.MONTHS)
else -> formatter.format(years, Direction.LAST, RelativeUnit.YEARS)
}
}
fun formatInvoiceExpiryRelative(
expirySeconds: ULong,
locale: Locale = Locale.getDefault(),
): String {
val seconds = expirySeconds.toLong()
if (seconds <= 0) return ""
val uLocale = ULocale.forLocale(locale)
val numberFormat = NumberFormat.getNumberInstance(uLocale)?.apply { maximumFractionDigits = 0 }
val formatter = RelativeDateTimeFormatter.getInstance(
uLocale,
numberFormat,
RelativeDateTimeFormatter.Style.LONG,
DisplayContext.CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE,
) ?: return ""
val minutes = seconds / Factor.SECONDS_TO_MINUTES.toLong()
val hours = minutes / Factor.MINUTES_TO_HOURS.toLong()
val days = hours / Factor.HOURS_TO_DAYS.toLong()
return when {
minutes < 1 -> formatter.format(seconds.toDouble(), Direction.NEXT, RelativeUnit.SECONDS)
hours < 1 -> formatter.format(minutes.toDouble(), Direction.NEXT, RelativeUnit.MINUTES)
days < 1 -> formatter.format(hours.toDouble(), Direction.NEXT, RelativeUnit.HOURS)
else -> formatter.format(days.toDouble(), Direction.NEXT, RelativeUnit.DAYS)
}
}
fun getDaysInMonth(month: LocalDate): List<LocalDate?> {
val firstDayOfMonth = LocalDate(month.year, month.month, Constants.FIRST_DAY_OF_MONTH)
val daysInMonth = month.month.toJavaMonth().length(isLeapYear(month.year))
// Get the day of week for the first day (1 = Monday, 7 = Sunday)
val firstDayOfWeek = firstDayOfMonth.dayOfWeek.ordinal + CalendarConstants.CALENDAR_WEEK_OFFSET
// Calculate offset (days before the first day)
// We want Sunday to be 0, so adjust accordingly
val offset = (firstDayOfWeek % CalendarConstants.DAYS_IN_WEEK_MOD)
val days = mutableListOf<LocalDate?>()
// Add empty spaces before the first day
repeat(offset) {
days.add(null)
}
// Add all days of the month
for (day in Constants.FIRST_DAY_OF_MONTH..daysInMonth) {
days.add(LocalDate(month.year, month.month, day))
}
// Add empty spaces to complete the last week (total should be multiple of 7)
while (days.size % CalendarConstants.DAYS_IN_WEEK_MOD != 0) {
days.add(null)
}
return days
}
fun isLeapYear(year: Int): Boolean {
return (year % Constants.LEAP_YEAR_DIVISOR_4 == 0 && year % Constants.LEAP_YEAR_DIVISOR_100 != 0) ||
(year % Constants.LEAP_YEAR_DIVISOR_400 == 0)
}
@OptIn(ExperimentalTime::class)
fun isDateInRange(
dateMillis: Long,
startMillis: Long?,
endMillis: Long?,
zone: TimeZone = TimeZone.currentSystemDefault(),
): Boolean {
if (startMillis == null) return false
val end = endMillis ?: startMillis
val normalizedDate = KInstant.fromEpochMilliseconds(dateMillis).toLocalDateTime(zone).date
val normalizedStart = KInstant.fromEpochMilliseconds(startMillis).toLocalDateTime(zone).date
val normalizedEnd = KInstant.fromEpochMilliseconds(end).toLocalDateTime(zone).date
return normalizedDate in normalizedStart..normalizedEnd
}
fun LocalDate.toMonthYearString(locale: Locale = Locale.getDefault()): String {
val formatter = SimpleDateFormat(DatePattern.MONTH_YEAR_FORMAT, locale)
val calendar = Calendar.getInstance()
calendar.set(year, month.number - CalendarConstants.MONTH_INDEX_OFFSET, Constants.FIRST_DAY_OF_MONTH)
return formatter.format(calendar.time)
}
fun LocalDate.minusMonths(months: Int): LocalDate =
toJavaLocalDate().minusMonths(months.toLong()).withDayOfMonth(1) // Always use first day of month for display
.toKotlinLocalDate()
fun LocalDate.plusMonths(months: Int): LocalDate =
toJavaLocalDate().plusMonths(months.toLong()).withDayOfMonth(1) // Always use first day of month for display
.toKotlinLocalDate()
@OptIn(ExperimentalTime::class)
fun LocalDate.endOfDay(zone: TimeZone = TimeZone.currentSystemDefault()): Long =
atStartOfDayIn(zone).plus(1.days).minus(1.milliseconds).toEpochMilliseconds()
fun utcDateFormatterOf(pattern: String) = SimpleDateFormat(pattern, Locale.US).apply {
timeZone = java.util.TimeZone.getTimeZone("UTC")
}
object DatePattern {
const val DATE_TIME = "dd/MM/yyyy, HH:mm"
const val INVOICE_EXPIRY = "MMM dd, h:mm a"
const val ACTIVITY_DATE = "MMMM d"
const val ACTIVITY_ROW_DATE = "MMMM d, HH:mm"
const val ACTIVITY_ROW_DATE_YEAR = "MMMM d yyyy, HH:mm"
const val ACTIVITY_TIME = "h:mm"
const val CHANNEL_DETAILS = "MMM d, yyyy, HH:mm"
const val LOG_FILE = "yyyy-MM-dd_HH-mm-ss"
const val LOG_LINE = "yyyy-MM-dd HH:mm:ss.SSS"
const val MONTH_YEAR_FORMAT = "MMMM yyyy"
const val DATE_FORMAT = "MMM d, yyyy"
const val WEEKDAY_FORMAT = "EEE"
}
private object Constants {
// Calendar
const val FIRST_DAY_OF_MONTH = 1
// Leap year calculation
const val LEAP_YEAR_DIVISOR_4 = 4
const val LEAP_YEAR_DIVISOR_100 = 100
const val LEAP_YEAR_DIVISOR_400 = 400
}
private object Factor {
const val MILLIS_TO_SECONDS = 1000.0
const val SECONDS_TO_MINUTES = 60.0
const val MINUTES_TO_HOURS = 60.0
const val HOURS_TO_DAYS = 24.0
const val DAYS_TO_WEEKS = 7.0
const val DAYS_TO_MONTHS = 30.0
const val DAYS_TO_YEARS = 365.0
}
private object Threshold {
const val SECONDS = 60
const val MINUTES = 60
const val HOURS = 24
const val YESTERDAY = 2
const val DAYS = 7
const val WEEKS = 4
const val MONTHS = 12
}
object CalendarConstants {
// Calendar grid
const val DAYS_IN_WEEK = 7
// Date formatting
const val WEEKDAY_ABBREVIATION_LENGTH = 3
// Calendar math
const val DAYS_IN_WEEK_MOD = 7
const val CALENDAR_WEEK_OFFSET = 1
const val MONTH_INDEX_OFFSET = 1
}