Pantrix UI

Tarih ve saat

Compose Multiplatform'da bir günün (yıl, ay, gün) ve günün bir saatinin ortak kodda neden kendiliğinden bulunmadığı, kütüphanenin bunun için ne verdiği, bir günün ve bir saatin bir uygulamanın kendi türüne nasıl çevrildiği, bir gün ile bir saatin nasıl bir an olduğu, dile göre nasıl yazıldıkları (12 ya da 24 saat dahil), "bugün"ün nasıl bilindiği ve saatle birlikte kayan bir aralığın ("son 15 dakika") nasıl tutulup okunduğu. Sorun Pantrix'e özgü değildir: bir tarih gösteren ya da seçtiren her Compose Multiplatform projesinde aynı şekilde görülür.

Burada söylenen her şey Kotlin 2.4.21'in standart kütüphanesinde ve bu depoda koşan testlerde görülmüştür; hangisinin nerede görüldüğü 8. bölümde yazılıdır.

1. Sorun

Belirti

Neden

Kotlin 2.4.21'in standart kütüphanesi kotlin.time paketinde Instant, Clock ve Duration verir; deneysel değillerdir, opt-in istemezler. Bir takvim günü, saat dilimleri ya da bir günü yazmak standart kütüphanede yoktur:

NeNerede bulunur
Bir an, şimdiStandart kütüphane: kotlin.time.Instant, Clock.System.now()
Takvim günü, saat dilimlerikotlinx-datetime (ayrı bir bağımlılık), java.time (JVM, Android), NSCalendar (iOS), Date (web)
Cihazın saatinin UTC'den farkıHer platformun kendi API'si: java.util.TimeZone, NSTimeZone, Date.getTimezoneOffset()
Ay ve gün adlarıHer platformun yerelleştirmesi; ortak kodda yok

2. Çözüm: dokuz parça

Foundation'da (com.pantrixui.foundation.date ve com.pantrixui.foundation.l10n):

  1. PantrixDate(year, month, day): saati ve saat dilimi olmayan bir gün. Kurulurken doğrulanır (13. ay, 30 Şubat reddedilir), sıralanabilir, ISO 8601 biçiminde yazılır ve okunur (2026-10-17, PantrixDate.parse). Hesapları: dayOfWeek (ISO: 1 Pazartesi, 7 Pazar), epochDays ve ofEpochDays (1970'ten gün sayısı), plusDays, plusMonths ve plusYears (ayın kısa olduğu yerde ayın son günü), daysInMonth, isLeapYear.

  2. PantrixDateRange(start, end): iki ucu dahil bir gün aralığı; sonu başından önce olamaz. day in range.

  3. Bugün ve ekranın saati: PantrixDate.today() cihazın saatinin gösterdiği gün; PantrixDate.of(moment, offset) bir anın, UTC'den offset ilerideki bir saatte hangi gün olduğu. Bir ekranda rememberToday(): ekranın saatinin günü, gece yarısı kendiliğinden yenilenir. Ekranın saati PantrixClock(clock, utcOffset)'tur, bir Clock ve bir fark işlevi; LocalPantrixClock onu verir, varsayılanı cihazınkidir (PantrixClock.System). rememberToday(), saat alanının seçicisi, elastic date picker ve grafiklerin anları onu okur; bir test ya da önizleme kendi saatini verir (3.4, 6).

  4. systemUtcOffset(moment) (com.pantrixui.foundation.platform): cihazın saatinin o anda UTC'den ne kadar ileride olduğu; yaz saati dahil. Her platformun kendi API'siyle.

  5. PantrixStrings'in tarih sözleri, her dilde: kısa ve uzun ay adları (month, longMonth), haftanın günleri (weekday, shortWeekday), bir gün (date: "17 Eki 2026"), haftanın günüyle tam bir gün (longDate: "17 Ekim 2026 Cumartesi"), bir ay (monthYear, longMonthYear: "Ekim 2026").

  6. PantrixTime(hour, minute, second = 0): günü ve saat dilimi olmayan, 24 saatlik bir saat. Kurulurken doğrulanır, sıralanabilir, ISO 8601 biçiminde yazılır ve okunur (14:05, 14:05:30, PantrixTime.parse); secondOfDay ve ofSecondOfDay, PantrixTime.of(moment, offset), PantrixTime.now(). Bir gün ile bir saat PantrixDate.at(time) ile cihazın saatinde bir an (Instant) olur; saatin ileri ve geri alındığı geceler dahil (3.7).

  7. PantrixStrings'in saat sözleri: time(hour, minute, is24Hour = …) bir saati çağıranın istediği düzenle yazar: true 24 saat (22:00, clock gibi), false 12 saat ve dilin sözleriyle günün yarısı ("10:00 PM", "ÖS 10:00", "٢:٠٥ م"). Hangi düzenin kullanılacağını paket bilmez, çağıran söyler. hour, minute, period, am, pm ve twelveHourTemplate bir saat seçicinin sözleridir.

  8. PantrixTimePoint ve PantrixTimeRange: saatle birlikte kayabilen bir zaman noktası ve iki ucu. Bir nokta şimdidir (Now), şimdiden bir süredir (Relative(-15, Minutes), bir birime yuvarlanabilir: Relative(-1, Days, roundTo = Days) dün) ya da bir andır (Absolute(instant)). resolve(now) noktaları o anki iki Instant'a çevirir: başlangıç aşağı, son yukarı yuvarlanır; sonu başından önce düşen bir aralık null'dır. Birimler PantrixTimeUnit'tir: saniyeden saate geçen zaman, günden yıla takvimin (yaz saati gecesi dahil). Uçlar Elasticsearch ve Kibana'nın tarih matematiğiyle yazılır ve okunur: toDateMath() (now-15m, now/d, 2026-10-17T14:15:00+03:00), PantrixTimePoint.parseDateMath.

  9. PantrixTimeWords (PantrixStrings.timeWords): bir sürenin sözleri, her dilin çoğul kurallarıyla: "~ 15 dakika önce", "Son 15 dakika", "Önümüzdeki 2 saat", ", güne yuvarlanmış". İngilizce (bir/çok), Türkçe (tek biçim) ve Arapça (CLDR'ın altı biçimi, Arap rakamları).

Bileşenlerde bunların üstünde PantrixDatePicker (date.datepicker) durur, bir gün ya da bir aralık seçtiren takvim; PantrixTimePicker (date.timepicker), sütunlardan bir saat seçtiren seçici; PantrixElasticDatePicker (date.elasticdatepicker), Elastic UI'ın EuiSuperDatePicker'ı gibi saatle kayan bir aralık seçtiren alan: hızlı menü, aralığın adı ya da iki ucu, Yenile ve Güncelle; PantrixDateTextField ile PantrixDateFilledTextField (date.datetextfield, date.datefilledtextfield), günü rakamlarla yazdıran ya da düğmesinin açtığı takvimden seçtiren metin alanları (3.10); ve PantrixTimeTextField ile PantrixTimeFilledTextField (date.timetextfield, date.timefilledtextfield), saati aynı biçimde yazdıran ya da saat seçiciden seçtiren alanlar (3.11). Dört alan tek bir iç çekirdeği paylaşır (date/common/PickerField.kt); her biri yalnız kendi rakam kurallarını, hatalarını ve seçicisini verir.

Kütüphaneye bir tarih bağımlılığı eklenmedi. PantrixDate ve PantrixTime üçer sayıdır; bir uygulama kendi türüne bir satırla çevirir (3.2, 3.7).

3. Pantrix ile

3.1. Bir gün seçtirmek

var due by remember { mutableStateOf<PantrixDate?>(null) }
PantrixDatePicker(
    selected = due,
    onSelect = { due = it },
    minDate = rememberToday(),
    tagKey = "due"
)

Takvim seçimi tutmaz: selected uygulamanındır, basılan gün onSelect'e gelir. Bir popover'da, bir dialog'da ya da bir sheet'te açmak uygulamanın işidir; demo dördünü de gösterir (DatePickerSamples.kt).

Takvim kendi genişliğinde durur (yedi günün yeri; telefonda platformun dokunma hedefi kadar), bu yüzden bir popover'ı ve bir dialog'u gereksiz genişletmez. Bir sheet'te ya da bir sayfanın genişliğinde Modifier.fillMaxWidth() verin: takvim o genişliği doldurur, sütunları DatePickerStyle.maxCellWidth'e (hücrenin bir buçuk katı) kadar genişler, satırları ve günlerin daireleri boyunu korur; daha geniş bir yerde ortada durur.

3.2. Kendi tarih türüne çevirmek

kotlinx-datetime (0.7 ve sonrası; daha eski sürümlerde month.number yerine monthNumber, day yerine dayOfMonth):

fun LocalDate.toPantrix(): PantrixDate = PantrixDate(year, month.number, day)
fun PantrixDate.toLocalDate(): LocalDate = LocalDate(year, month, day)

java.time (JVM, Android):

fun java.time.LocalDate.toPantrix(): PantrixDate = PantrixDate(year, monthValue, dayOfMonth)
fun PantrixDate.toJavaLocalDate(): java.time.LocalDate = java.time.LocalDate.of(year, month, day)

Bir sunucuyla ya da bir JSON'la ISO 8601 metniyle: PantrixDate.parse("2026-10-17") ve date.toString().

Bir sayıyla (veritabanı, tercihler): date.epochDays ve PantrixDate.ofEpochDays(days).

3.3. Bir günü yazmak

val strings = PantrixTheme.strings
strings.date(day.day, day.month, day.year)       // "Oct 17, 2026", "17 Eki 2026", "١٧ أكتوبر ٢٠٢٦"
strings.longDate(day.day, day.month, day.year)   // "Saturday, October 17, 2026", "17 Ekim 2026 Cumartesi"
strings.longMonthYear(day.month, day.year)       // "October 2026", "Ekim 2026"
strings.weekday(day.dayOfWeek)                   // "Saturday", "Cumartesi"

Bir uygulamanın kendi paketi bu alanları kendi diliyle verir: PantrixStrings(longMonthNames = …, weekdayNames = …). On iki ay ve yedi gün olmayan bir paket reddedilir. Haftanın başladığı gün bir sözcük değil, dilin bir alışkanlığıdır: PantrixLocale.firstDayOfWeek'tedir (Türkçede Pazartesi, Arapçada Cumartesi, İngilizcede ve öteki dillerde Pazar) ve bir yerel ayar onu kendi de verebilir: PantrixLocale("de", LayoutDirection.Ltr, firstDayOfWeek = 1).

3.4. Bugün

3.5. Bir aralık

var stay by remember { mutableStateOf<PantrixDateRange?>(null) }
PantrixDatePicker(selectedRange = stay, onRangeSelect = { stay = it }, tagKey = "stay")
val nights = stay?.let { it.end.epochDays - it.start.epochDays }

3.6. Bir saat seçtirmek

var at by remember { mutableStateOf<PantrixTime?>(null) }
PantrixTimePicker(
    selected = at,
    onSelect = { at = it },
    minTime = PantrixTime(9, 0),
    maxTime = PantrixTime(17, 45),
    minuteStep = 15,
    is24Hour = true,
    tagKey = "at"
)

Seçici de seçimi tutmaz: bir değere basmak bütün bir saati onSelect'e verir, öteki sütun olduğu gibi kalır. is24Hour seçicinin şeklini verir: true iki sütun (00–23 ve dakikalar, 22:00), false üç sütun (12–11, dakikalar ve AM/PM, 10:00 PM). Varsayılan her dilde 24 saattir. Seçilen saat her iki durumda da 24 saatlik bir PantrixTime'dır; yalnızca gösterimi değişir. Ekranda yazarken aynısını verin: strings.time(at.hour, at.minute, is24Hour = …).

minTime maxTime'dan sonraysa seçilebilen saatler gece yarısını geçer (22:00–06:00 bir gece). Sınırlar saat ve dakikalarıyla okunur, saniyeleri değil.

3.7. Gün ve saatten bir an

val moment: Instant = day.at(time)                  // cihazın saatinde
val back = PantrixDate.of(moment) to PantrixTime.of(moment)
val inIstanbul = day.at(time) { 3.hours }           // UTC'den üç saat ileride sabit bir saatte

Saatin ileri alındığı gecede hiç gösterilmeyen bir saat (ikiden üçe geçilen gecede 02:30), atlanan saat kadar ilerisidir: 03:30. Saatin geri alındığı gecede iki kez gösterilen bir saat ikisinden ilkidir. İkisi de java.time'ın ZonedDateTime.of ile aldığı andır.

kotlinx-datetime ve java.time ile:

fun LocalTime.toPantrix(): PantrixTime = PantrixTime(hour, minute, second)
fun PantrixTime.toLocalTime(): LocalTime = LocalTime(hour, minute, second)
fun java.time.LocalTime.toPantrix(): PantrixTime = PantrixTime(hour, minute, second)
fun PantrixTime.toJavaLocalTime(): java.time.LocalTime = java.time.LocalTime.of(hour, minute, second)

3.8. Saatle kayan bir aralık

val lastQuarterHour = PantrixTimeRange(PantrixTimePoint.Relative(-15, PantrixTimeUnit.Minutes), PantrixTimePoint.Now)
var range by remember { mutableStateOf(lastQuarterHour) }
PantrixElasticDatePicker(
    range = range,
    onRangeChange = { range = it },
    recentlyUsed = rememberRecentRanges(),
    onRefresh = { reload(range) },
    tagKey = "dashboard"
)

// Veriyi çekerken, her seferinde şimdiye göre:
val window: ClosedRange<Instant>? = range.resolve()

Aralık bir değerdir, bir an değil: "son 15 dakika" her resolve()'da yeniden okunur. Bir sorguya giden iki Instant her yenilemede, onRefresh içinde okunmalıdır; bir kez okunup saklanan pencere kaymaz.

Otomatik yenileme, zaman penceresi ve menünün parçaları

var refresh by remember { mutableStateOf(PantrixAutoRefresh(every = 30)) }
PantrixElasticDatePicker(
    range = range,
    onRangeChange = { range = it },
    onRefresh = { reload(range) },
    tagKey = "dashboard",
    autoRefresh = refresh,
    onAutoRefreshChange = { refresh = it },
    minAutoRefresh = 5.seconds,
    timeWindowButtons = ElasticTimeWindowButtons()
)

3.9. Bir adreste tarih matematiği

val (from, to) = range.toDateMath()                         // "now-15m", "now"
val url = "/logs?from=$from&to=$to"
val back = PantrixTimeRange(
    PantrixTimePoint.parseDateMath(from) ?: PantrixTimePoint.Now,
    PantrixTimePoint.parseDateMath(to) ?: PantrixTimePoint.Now
)

Okunanlar: now, ardından bir işlem (now-15m, now+2h; Kibana gibi sayısız now-d) ve bir yuvarlama (now-1d/d, now/w, Elasticsearch gibi /1d); birimler y M w d h m s ve saat için H. Bir an ISO 8601'in UTC farkıyla ya da Z ile yazdığı biçimdir; saniyenin kesri üçerli basamaklarla korunur (14:15:00.250+03:00). Başka her şey null'dır: iki işlem (now-1d/d+8h), ||'li bir tarih, farksız ya da yalnız gün olan bir ISO metni.

3.10. Bir günü yazdırmak: tarih alanı

Kullanıcının bildiği bir gün (doğum günü, bir sözleşmenin başlangıcı) takvimde aranmaktan çok yazılır. PantrixDateTextField bir metin alanıdır: gün rakamlarla yazılır, kutunun sonundaki düğme takvimi açar.

var birthday by remember { mutableStateOf<PantrixDate?>(null) }
PantrixDateTextField(
    value = birthday,
    onValueChange = { birthday = it },
    label = stringResource(Res.string.birthday),
    maxDate = rememberToday(),
    tagKey = "birthday"
)

3.11. Bir saati yazdırmak: saat alanı

Kullanıcının bildiği bir saat (bir toplantının başlangıcı, bir alarm) sütunlarda kaydırılmaktan çok yazılır. PantrixTimeTextField tarih alanının saat için olanıdır: saat rakamlarla yazılır, kutunun sonundaki düğme PantrixTimePicker'ı açar.

var start by remember { mutableStateOf<PantrixTime?>(null) }
PantrixTimeTextField(
    value = start,
    onValueChange = { start = it },
    label = stringResource(Res.string.meeting_start),
    minuteStep = 15,
    tagKey = "start"
)

4. Pantrix olmadan

Kütüphanesiz bir proje için en küçük parçalar. Gün hesabı Howard Hinnant'ın sayımıdır (C++ ve Rust'ın standart kütüphanelerinin kullandığı), her miladi gün için kesindir:

/** 1970-01-01'den bu güne kadar geçen gün sayısı; öncesi eksi. */
fun daysFromCivil(year: Int, month: Int, day: Int): Long {
    val y = if (month <= 2) year.toLong() - 1 else year.toLong()
    val era = y.floorDiv(400)
    val yearOfEra = y - era * 400
    val dayOfYear = (153 * (month + (if (month > 2) -3 else 9)) + 2) / 5 + day - 1
    val dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear
    return era * 146_097 + dayOfEra - 719_468
}

/** ISO 8601'in günü: 1 Pazartesi, 7 Pazar. 1970-01-01 bir Perşembe'ydi. */
fun dayOfWeek(year: Int, month: Int, day: Int): Int = (daysFromCivil(year, month, day) + 3).mod(7) + 1

Cihazın saatinin UTC'den farkı, her platformda bir actual:

// commonMain
expect fun systemUtcOffset(moment: Instant): Duration

// jvmMain ve androidMain
actual fun systemUtcOffset(moment: Instant): Duration =
    java.util.TimeZone.getDefault().getOffset(moment.toEpochMilliseconds()).milliseconds

// iosMain
actual fun systemUtcOffset(moment: Instant): Duration =
    NSTimeZone.localTimeZone.secondsFromGMTForDate(NSDate.dateWithTimeIntervalSince1970(moment.epochSeconds.toDouble())).seconds

// webMain (js ve wasmJs): Date UTC'nin kaç dakika ileride olduğunu söyler, işareti ters
@OptIn(ExperimentalWasmJsInterop::class)
private fun minutesUtcIsAhead(milliseconds: Double): Double = js("new Date(milliseconds).getTimezoneOffset()")
actual fun systemUtcOffset(moment: Instant): Duration =
    -minutesUtcIsAhead(moment.toEpochMilliseconds().toDouble()).roundToInt().minutes

Bugün: (Clock.System.now() + systemUtcOffset(now)).epochSeconds.floorDiv(86_400) günüdür; bu sayıyı yıla, aya, güne çeviren ters hesap PantrixDate.kt'deki civilFromDays'tir.

5. Yardımcının yapmadıkları

6. Test etmek

7. Tuzaklar

8. Doğrulama

Testler

NeNerede
Günün doğrulanması, haftanın günü, 1970'ten gün sayısı (±800.000 gün), ay ve yıl ekleme, sıralama, ISO yazma ve okuma, bir anın günü, aralıkui/foundation/src/commonTest/.../date/PantrixDateTest.kt (JVM, Android, iOS, JS, Wasm)
Gece yarısına kadar bekleme, gece yarısı dönen günTodayTest
Ekranın saati: günü ve saati, yaz saatinin bittiği an, eşitlik, varsayılanı, rememberToday'in okuduğu saat ve değişen saatPantrixClockTest
Ekranın saatini okuyan bileşenler: saat alanının seçicisi, elastic date picker'ın açık API'si, grafikler (kartezyen, polar, radar, Gantt)TimeTextFieldTest, ElasticDatePickerTest, ChartScreenClockTest
Ay ve gün adları, uzun tarih, haftanın ilk günü, yanlış paketlerin reddiPantrixStringsTest
Cihazın saat farkıSystemTimeTest, SystemTimeOnTheDesktopTest
Saatin doğrulanması, sıralama, ISO yazma ve okuma, bir anın saati, gün ve saatten bir an (yaz saati geceleri dahil)ui/foundation/src/commonTest/.../date/PantrixTimeTest.kt
12 ve 24 saatlik yazım, saat sözleriPantrixStringsTest
Saat seçici: sütunlar, sınırlar, gece aralığı, basış, tuşlar, yazılan rakamlar, Tab durakları, ortalama, ekran okuyucu, renkler, stilui/components/src/commonTest/.../TimePicker*Test.kt, TimeColumnsTest.kt
Takvim: ayın yerleri, tuşlar, Tab durağı, aralık, yıllar, ekran okuyucu, renkler, stilui/components/src/commonTest/.../DatePicker*Test.kt, CalendarMonthTest.kt
Tarih alanı: rakamlar ve ayraçlar, biçimler, Arap rakamları, yarım gün, kendi hataları, uygulamanın hatası, dışarıdan gelen değer, durum, takvimden seçim ve olaylar, Alt+↓, Escape, Enter, salt okunur, devre dışı, sheet ve dialog, düzenin söylenişi, form, dolu kutuui/components/src/commonTest/.../DateTextFieldTest.kt, DateFieldFormatTest.kt
Saat alanı: iki nokta, öne sıfır, kendi hataları, sınırlar ve gece yarısını geçen aralık, dışarıdan gelen değer, 12 saat ve günün yarısının düğmesi, A/P, seçicide her basış ve olaylar, Alt+↓ ile şimdiki saate odak, Enter'la kapanma, salt okunur, yapıştırma, durum, form, dolu kutu, düzenin söylenişiui/components/src/commonTest/.../TimeTextFieldTest.kt, TimeFieldTextTest.kt
Grafiklerin zaman ekseni (aynı gün hesabını kullanır)ChartTimeTest, ChartTimeAxisTest
Kayan noktalar: taşıma, yuvarlama, haftanın başı (1, 6, 7), yaz saati gecesi, aralığın okunması, tarih matematiği yazma ve okuma, saniyenin kesriui/foundation/src/commonTest/.../date/PantrixTimePointTest.kt
Sürenin sözleri ve çoğulları (İngilizce, Türkçe, Arapça)PantrixTimeWordsTest
Elastic date picker: aralığın adı ve uçları, ada basmak, hızlı menü, ‹ ›, bekleyen düzenleme, sırası bozuk uçlar, None, Yenile ve onRefresh, loading, enabled, yeni aralık, sekmenin ucu çevirmesi, takvim, yarım saatlik saatler ve yazılan an, 12 saat, sheet, dialog, başlıksız panel, dar yerde simge, renkler, stil, son kullanılanlar, bir anın yazılıp okunmasıui/components/src/commonTest/.../ElasticDatePicker*Test.kt

Elle bakılanlar

Doğrulanmayanlar

9. Bu depoda nerede

10. Açık kalanlar

docs/OPEN_ITEMS.md'de: saat alanları (PantrixTimeTextField, PantrixTimeFilledTextField; iskeletleri duruyor), iki uçlu bir tarih alanı, adlı saat dilimleri, cihazın 12/24 saat ayarı, metinden dile göre tarih ve saat okumak; elastic date picker'da elle basılan Yenile'nin hatası ve dışarı basışı geçiren popover seçeneğinin genel yapılması.