Skip to content

Commit 0a120da

Browse files
authored
Merge pull request #446 from synonymdev/feat/custom-date-picker
Feat/custom date picker
2 parents c406d80 + 27dc6c6 commit 0a120da

File tree

4 files changed

+681
-61
lines changed

4 files changed

+681
-61
lines changed

.github/copilot-instructions.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ When performing a code review, check that Material3 design guidelines are follow
2828

2929
When performing a code review, ensure proper state management patterns with `MutableStateFlow` and `StateFlow`.
3030

31+
When performing a code review, check if modifiers, if present, are in last place in args list
32+
3133
## Code Quality & Readability
3234

3335
When performing a code review, focus on readability and avoid nested if-else, replacing with early return wherever possible.

app/src/main/java/to/bitkit/ext/DateTime.kt

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,23 @@ package to.bitkit.ext
22

33
import android.icu.text.DateFormat
44
import android.icu.util.ULocale
5+
import kotlinx.datetime.LocalDate
6+
import kotlinx.datetime.TimeZone
7+
import kotlinx.datetime.atStartOfDayIn
8+
import kotlinx.datetime.toJavaLocalDate
9+
import kotlinx.datetime.toKotlinLocalDate
10+
import kotlinx.datetime.toLocalDateTime
11+
import java.text.SimpleDateFormat
512
import java.time.Instant
613
import java.time.LocalDateTime
714
import java.time.ZoneId
815
import java.time.format.DateTimeFormatter
916
import java.time.temporal.ChronoUnit
17+
import java.util.Calendar
1018
import java.util.Date
1119
import java.util.Locale
20+
import kotlin.time.Duration.Companion.days
21+
import kotlin.time.Duration.Companion.milliseconds
1222

1323
fun nowTimestamp(): Instant = Instant.now().truncatedTo(ChronoUnit.SECONDS)
1424

@@ -36,6 +46,82 @@ fun Long.toLocalizedTimestamp(): String {
3646
return formatter.format(Date(this))
3747
}
3848

49+
fun getDaysInMonth(month: LocalDate): List<LocalDate?> {
50+
val firstDayOfMonth = LocalDate(month.year, month.month, CalendarConstants.FIRST_DAY_OF_MONTH)
51+
val daysInMonth = month.month.length(isLeapYear(month.year))
52+
53+
// Get the day of week for the first day (1 = Monday, 7 = Sunday)
54+
val firstDayOfWeek = firstDayOfMonth.dayOfWeek.ordinal + CalendarConstants.CALENDAR_WEEK_OFFSET
55+
56+
// Calculate offset (days before the first day)
57+
// We want Sunday to be 0, so adjust accordingly
58+
val offset = (firstDayOfWeek % CalendarConstants.DAYS_IN_WEEK_MOD)
59+
60+
val days = mutableListOf<LocalDate?>()
61+
62+
// Add empty spaces before the first day
63+
repeat(offset) {
64+
days.add(null)
65+
}
66+
67+
// Add all days of the month
68+
for (day in CalendarConstants.FIRST_DAY_OF_MONTH..daysInMonth) {
69+
days.add(LocalDate(month.year, month.month, day))
70+
}
71+
72+
// Add empty spaces to complete the last week (total should be multiple of 7)
73+
while (days.size % CalendarConstants.DAYS_IN_WEEK_MOD != 0) {
74+
days.add(null)
75+
}
76+
77+
return days
78+
}
79+
80+
fun isLeapYear(year: Int): Boolean {
81+
return (year % CalendarConstants.LEAP_YEAR_DIVISOR_4 == 0 && year % CalendarConstants.LEAP_YEAR_DIVISOR_100 != 0) ||
82+
(year % CalendarConstants.LEAP_YEAR_DIVISOR_400 == 0)
83+
}
84+
85+
fun isDateInRange(dateMillis: Long, startMillis: Long?, endMillis: Long?): Boolean {
86+
if (startMillis == null) return false
87+
val end = endMillis ?: startMillis
88+
89+
val normalizedDate = kotlinx.datetime.Instant.fromEpochMilliseconds(dateMillis)
90+
.toLocalDateTime(TimeZone.currentSystemDefault()).date
91+
val normalizedStart = kotlinx.datetime.Instant.fromEpochMilliseconds(startMillis)
92+
.toLocalDateTime(TimeZone.currentSystemDefault()).date
93+
val normalizedEnd = kotlinx.datetime.Instant.fromEpochMilliseconds(end)
94+
.toLocalDateTime(TimeZone.currentSystemDefault()).date
95+
96+
return normalizedDate >= normalizedStart && normalizedDate <= normalizedEnd
97+
}
98+
99+
fun LocalDate.toMonthYearString(): String {
100+
val formatter = SimpleDateFormat(DatePattern.MONTH_YEAR_FORMAT, Locale.getDefault())
101+
val calendar = Calendar.getInstance()
102+
calendar.set(year, monthNumber - CalendarConstants.MONTH_INDEX_OFFSET, CalendarConstants.FIRST_DAY_OF_MONTH)
103+
return formatter.format(calendar.time)
104+
}
105+
106+
fun LocalDate.minusMonths(months: Int): LocalDate =
107+
this.toJavaLocalDate()
108+
.minusMonths(months.toLong())
109+
.withDayOfMonth(1) // Always use first day of month for display
110+
.toKotlinLocalDate()
111+
112+
fun LocalDate.plusMonths(months: Int): LocalDate =
113+
this.toJavaLocalDate()
114+
.plusMonths(months.toLong())
115+
.withDayOfMonth(1) // Always use first day of month for display
116+
.toKotlinLocalDate()
117+
118+
fun LocalDate.endOfDay(): Long {
119+
return this.atStartOfDayIn(TimeZone.currentSystemDefault())
120+
.plus(1.days)
121+
.minus(1.milliseconds)
122+
.toEpochMilliseconds()
123+
}
124+
39125
object DatePattern {
40126
const val DATE_TIME = "dd/MM/yyyy, HH:mm"
41127
const val INVOICE_EXPIRY = "MMM dd, h:mm a"
@@ -45,4 +131,32 @@ object DatePattern {
45131
const val ACTIVITY_TIME = "h:mm"
46132
const val LOG_FILE = "yyyy-MM-dd_HH-mm-ss"
47133
const val CHANNEL_DETAILS = "MMM d, yyyy, HH:mm"
134+
135+
const val MONTH_YEAR_FORMAT = "MMMM yyyy"
136+
const val DATE_FORMAT = "MMM d, yyyy"
137+
const val WEEKDAY_FORMAT = "EEE"
48138
}
139+
140+
object CalendarConstants {
141+
142+
// Calendar grid
143+
const val DAYS_IN_WEEK = 7
144+
const val FIRST_DAY_OF_MONTH = 1
145+
146+
// Date formatting
147+
const val WEEKDAY_ABBREVIATION_LENGTH = 3
148+
149+
// Calendar math
150+
const val DAYS_IN_WEEK_MOD = 7
151+
const val CALENDAR_WEEK_OFFSET = 1
152+
const val MONTH_INDEX_OFFSET = 1
153+
154+
// Leap year calculation
155+
const val LEAP_YEAR_DIVISOR_4 = 4
156+
const val LEAP_YEAR_DIVISOR_100 = 100
157+
const val LEAP_YEAR_DIVISOR_400 = 400
158+
159+
// Preview
160+
const val PREVIEW_DAYS_AGO = 7
161+
}
162+

0 commit comments

Comments
 (0)