Skip to content

Commit 554c68c

Browse files
authored
Add 12-hour format support with AM/PM and hourIn12
Add 12-hour format support with AM/PM and hourIn12
2 parents db4c684 + b106047 commit 554c68c

File tree

8 files changed

+261
-2
lines changed

8 files changed

+261
-2
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ Any missing field (like time) will default to current system time.
117117
| `d` | 1 or 2-digit day | `9` |
118118
| `HH` | Hour (24h, 2-digit) | `14` |
119119
| `H` | Hour (24h) | `14` |
120+
| `hh` | Hour (12h, 2-digit) | `09` |
121+
| `h` | Hour (12h) | `9` |
120122
| `mm` | Minutes (2-digit) | `03` |
121123
| `m` | Minutes | `3` |
122124
| `ss` | Seconds (2-digit) | `07` |

jalalidate/src/commonMain/kotlin/ir/amirroid/jalalidate/date/JalaliDateTime.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ public class JalaliDateTime {
2828
public val minute: Int
2929
public val second: Int
3030

31+
public val hourIn12: Int
32+
get() {
33+
val h = hour % 12
34+
return if (h == 0) 12 else h
35+
}
36+
3137
public val gregorian: LocalDateTime
3238
get() = algorithm.toGregorian(this)
3339

jalalidate/src/commonMain/kotlin/ir/amirroid/jalalidate/formatter/FormatPart.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,35 @@ internal class NumericPart(
5656
}
5757
}
5858

59+
internal class AmPmPart : FormatPart {
60+
override val name: String = "amPm"
61+
62+
private val extractor: (JalaliDateTime) -> String = { date ->
63+
if (date.hour < 12) "AM" else "PM"
64+
}
65+
66+
private val parser: (String) -> Int? = { text ->
67+
when (text.uppercase()) {
68+
"AM" -> 0
69+
"PM" -> 1
70+
else -> null
71+
}
72+
}
73+
74+
override fun format(date: JalaliDateTime): String = extractor(date)
75+
76+
override fun parse(input: String, pos: Int): ParseResult {
77+
val match = Regex("(AM|PM)", RegexOption.IGNORE_CASE)
78+
.find(input, pos)
79+
?: throw IllegalArgumentException("Expected AM/PM at position $pos")
80+
81+
val value = parser(match.value)
82+
?: throw IllegalArgumentException("Invalid AM/PM value '${match.value}'")
83+
84+
return ParseResult(value, pos + match.value.length)
85+
}
86+
}
87+
5988
internal sealed class NamedPart(
6089
private val full: Boolean,
6190
private val locale: Locale

jalalidate/src/commonMain/kotlin/ir/amirroid/jalalidate/formatter/JalaliDateTimeFormatter.kt

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ir.amirroid.jalalidate.formatter
33
import ir.amirroid.jalalidate.algorithm.JalaliAlgorithm
44
import ir.amirroid.jalalidate.configuration.JalaliDateGlobalConfiguration
55
import ir.amirroid.jalalidate.date.JalaliDateTime
6+
import kotlin.compareTo
67

78
public enum class Padding { ZERO, SPACE }
89

@@ -13,7 +14,7 @@ public class JalaliDateTimeFormatter {
1314
private var locale = JalaliDateGlobalConfiguration.formatterLocale
1415

1516
public fun byUnicodePattern(pattern: String): JalaliDateTimeFormatter {
16-
val regex = "(yyyy|yy|MMMM|MMM|MM|M|dd|d|HH|H|mm|m|ss|s|EEEE|EEE|E)".toRegex()
17+
val regex = "(yyyy|yy|MMMM|MMM|MM|M|dd|d|HH|H|hh|h|mm|m|ss|s|EEEE|EEE|E|a)".toRegex()
1718
var lastIndex = 0
1819
for (match in regex.findAll(pattern)) {
1920
if (lastIndex < match.range.first) {
@@ -44,6 +45,8 @@ public class JalaliDateTimeFormatter {
4445
public fun monthOneDigit(): JalaliDateTimeFormatter =
4546
addNumericPart("month", { it.jalaliMonth }, 1, Padding.ZERO)
4647

48+
public fun amPm(): JalaliDateTimeFormatter = addAmPm()
49+
4750
public fun monthFullName(): JalaliDateTimeFormatter {
4851
parts += MonthNamePart(full = true, locale = locale)
4952
return this
@@ -98,6 +101,11 @@ public class JalaliDateTimeFormatter {
98101
return this
99102
}
100103

104+
private fun addAmPm(): JalaliDateTimeFormatter {
105+
parts.add(AmPmPart())
106+
return this
107+
}
108+
101109
private fun mapPatternToFormatPart(token: String): FormatPart = when (token) {
102110
"yyyy" -> NumericPart("year", { it.jalaliYear }, 4, Padding.ZERO)
103111
"yy" -> NumericPart("yearShort", { it.jalaliYear % 100 }, 2, Padding.ZERO)
@@ -119,12 +127,17 @@ public class JalaliDateTimeFormatter {
119127
"HH" -> NumericPart("hour", { it.hour }, 2, Padding.ZERO)
120128
"H" -> NumericPart("hour", { it.hour }, 1, Padding.ZERO)
121129

130+
"hh" -> NumericPart("hour", { it.hourIn12 }, 2, Padding.ZERO)
131+
"h" -> NumericPart("hour", { it.hourIn12 }, 1, Padding.ZERO)
132+
122133
"mm" -> NumericPart("minute", { it.minute }, 2, Padding.ZERO)
123134
"m" -> NumericPart("minute", { it.minute }, 1, Padding.ZERO)
124135

125136
"ss" -> NumericPart("second", { it.second }, 2, Padding.ZERO)
126137
"s" -> NumericPart("second", { it.second }, 1, Padding.ZERO)
127138

139+
"a" -> AmPmPart()
140+
128141
else -> LiteralPart(token)
129142
}
130143

@@ -151,10 +164,18 @@ public class JalaliDateTimeFormatter {
151164
val year = values["year"] ?: 1300
152165
val month = values["month"] ?: 1
153166
val day = values["day"] ?: 1
154-
val hour = values["hour"] ?: 0
167+
var hour = values["hour"] ?: 0
155168
val minute = values["minute"] ?: 0
156169
val second = values["second"] ?: 0
157170

171+
values["amPm"]?.let { amPm ->
172+
hour = when (amPm) {
173+
0 -> if (hour == 12) 0 else hour
174+
1 -> if (hour < 12) hour + 12 else hour
175+
else -> hour
176+
}
177+
}
178+
158179
return JalaliDateTime(year, month, day, hour, minute, second, algorithm)
159180
}
160181
}

jalalidate/src/commonTest/kotlin/ir/amirroid/jalalidate/birashk/FormatTest.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,40 @@ class FormatTest {
148148
val formatted = date.format { byUnicodePattern("E") }
149149
assertEquals("1", formatted)
150150
}
151+
152+
153+
@Test
154+
fun testFormatAmMarker() {
155+
val date = JalaliDateTime(1402, 7, 15, 9, 30, 0)
156+
val formatted = date.format { byUnicodePattern("hh:mm a") }
157+
assertEquals("09:30 AM", formatted)
158+
}
159+
160+
@Test
161+
fun testFormatPmMarker() {
162+
val date = JalaliDateTime(1402, 7, 15, 18, 45, 0)
163+
val formatted = date.format { byUnicodePattern("hh:mm a") }
164+
assertEquals("06:45 PM", formatted)
165+
}
166+
167+
@Test
168+
fun testAmPmAtBoundaryNoon() {
169+
val date = JalaliDateTime(1402, 7, 15, 12, 0, 0)
170+
val formatted = date.format { byUnicodePattern("hh:mm a") }
171+
assertEquals("12:00 PM", formatted)
172+
}
173+
174+
@Test
175+
fun testAmPmAtBoundaryMidnight() {
176+
val date = JalaliDateTime(1402, 7, 15, 0, 0, 0)
177+
val formatted = date.format { byUnicodePattern("hh:mm a") }
178+
assertEquals("12:00 AM", formatted)
179+
}
180+
181+
@Test
182+
fun testFullPatternWithAmPm() {
183+
val date = JalaliDateTime(1402, 1, 1, 23, 15, 0)
184+
val formatted = date.format { byUnicodePattern("yyyy/MM/dd hh:mm a") }
185+
assertEquals("1402/01/01 11:15 PM", formatted)
186+
}
151187
}

jalalidate/src/commonTest/kotlin/ir/amirroid/jalalidate/birashk/ParseTest.kt

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package ir.amirroid.jalalidate.birashk
22

33
import ir.amirroid.jalalidate.algorithm.defaults.BirashkAlgorithm
4+
import ir.amirroid.jalalidate.date.JalaliDateTime
5+
import ir.amirroid.jalalidate.format
46
import ir.amirroid.jalalidate.formatter.JalaliDateTimeFormatter
57
import kotlin.test.Test
68
import kotlin.test.assertEquals
@@ -219,4 +221,68 @@ class ParseTest {
219221
assertEquals(6, date.jalaliMonth)
220222
assertEquals(9, date.jalaliDay)
221223
}
224+
225+
@Test
226+
fun testParseAmPattern() {
227+
val input = "1402/07/15 09:30:00 AM"
228+
val formatter = JalaliDateTimeFormatter()
229+
.byUnicodePattern("yyyy/MM/dd hh:mm:ss a")
230+
231+
val date = formatter.parse(input, algorithm)
232+
assertEquals(1402, date.jalaliYear)
233+
assertEquals(7, date.jalaliMonth)
234+
assertEquals(15, date.jalaliDay)
235+
assertEquals(9, date.hour)
236+
assertEquals(30, date.minute)
237+
assertEquals(0, date.second)
238+
}
239+
240+
@Test
241+
fun testParsePmPattern() {
242+
val input = "1402/07/15 09:30:00 PM"
243+
val formatter = JalaliDateTimeFormatter()
244+
.byUnicodePattern("yyyy/MM/dd hh:mm:ss a")
245+
246+
val date = formatter.parse(input, algorithm)
247+
assertEquals(1402, date.jalaliYear)
248+
assertEquals(7, date.jalaliMonth)
249+
assertEquals(15, date.jalaliDay)
250+
assertEquals(21, date.hour)
251+
assertEquals(30, date.minute)
252+
assertEquals(0, date.second)
253+
}
254+
255+
@Test
256+
fun testParseNoonBoundary() {
257+
val input = "1402/07/15 12:00:00 PM"
258+
val formatter = JalaliDateTimeFormatter()
259+
.byUnicodePattern("yyyy/MM/dd hh:mm:ss a")
260+
261+
val date = formatter.parse(input, algorithm)
262+
assertEquals(12, date.hour)
263+
}
264+
265+
@Test
266+
fun testParseMidnightBoundary() {
267+
val input = "1402/07/15 12:00:00 AM"
268+
val formatter = JalaliDateTimeFormatter()
269+
.byUnicodePattern("yyyy/MM/dd hh:mm:ss a")
270+
271+
val date = formatter.parse(input, algorithm)
272+
assertEquals(0, date.hour)
273+
}
274+
275+
@Test
276+
fun testParseAmPmWithTextMixPattern() {
277+
val input = "شنبه 1402/01/01 11:15 PM"
278+
val formatter = JalaliDateTimeFormatter()
279+
.byUnicodePattern("EEEE yyyy/MM/dd hh:mm a")
280+
281+
val date = formatter.parse(input, algorithm)
282+
assertEquals(1402, date.jalaliYear)
283+
assertEquals(1, date.jalaliMonth)
284+
assertEquals(1, date.jalaliDay)
285+
assertEquals(23, date.hour)
286+
assertEquals(15, date.minute)
287+
}
222288
}

jalalidate/src/commonTest/kotlin/ir/amirroid/jalalidate/khayyam/FormatTest.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,39 @@ class FormatTest {
147147
val formatted = date.format { byUnicodePattern("E") }
148148
assertEquals("1", formatted)
149149
}
150+
151+
@Test
152+
fun testFormatAmMarker() {
153+
val date = JalaliDateTime(1402, 7, 15, 9, 30, 0)
154+
val formatted = date.format { byUnicodePattern("hh:mm a") }
155+
assertEquals("09:30 AM", formatted)
156+
}
157+
158+
@Test
159+
fun testFormatPmMarker() {
160+
val date = JalaliDateTime(1402, 7, 15, 18, 45, 0)
161+
val formatted = date.format { byUnicodePattern("hh:mm a") }
162+
assertEquals("06:45 PM", formatted)
163+
}
164+
165+
@Test
166+
fun testAmPmAtBoundaryNoon() {
167+
val date = JalaliDateTime(1402, 7, 15, 12, 0, 0)
168+
val formatted = date.format { byUnicodePattern("hh:mm a") }
169+
assertEquals("12:00 PM", formatted)
170+
}
171+
172+
@Test
173+
fun testAmPmAtBoundaryMidnight() {
174+
val date = JalaliDateTime(1402, 7, 15, 0, 0, 0)
175+
val formatted = date.format { byUnicodePattern("hh:mm a") }
176+
assertEquals("12:00 AM", formatted)
177+
}
178+
179+
@Test
180+
fun testFullPatternWithAmPm() {
181+
val date = JalaliDateTime(1402, 1, 1, 23, 15, 0)
182+
val formatted = date.format { byUnicodePattern("yyyy/MM/dd hh:mm a") }
183+
assertEquals("1402/01/01 11:15 PM", formatted)
184+
}
150185
}

jalalidate/src/commonTest/kotlin/ir/amirroid/jalalidate/khayyam/ParseTest.kt

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,68 @@ class ParseTest {
219219
assertEquals(6, date.jalaliMonth)
220220
assertEquals(9, date.jalaliDay)
221221
}
222+
223+
@Test
224+
fun testParseAmPattern() {
225+
val input = "1402/07/15 09:30:00 AM"
226+
val formatter = JalaliDateTimeFormatter()
227+
.byUnicodePattern("yyyy/MM/dd hh:mm:ss a")
228+
229+
val date = formatter.parse(input, algorithm)
230+
assertEquals(1402, date.jalaliYear)
231+
assertEquals(7, date.jalaliMonth)
232+
assertEquals(15, date.jalaliDay)
233+
assertEquals(9, date.hour)
234+
assertEquals(30, date.minute)
235+
assertEquals(0, date.second)
236+
}
237+
238+
@Test
239+
fun testParsePmPattern() {
240+
val input = "1402/07/15 09:30:00 PM"
241+
val formatter = JalaliDateTimeFormatter()
242+
.byUnicodePattern("yyyy/MM/dd hh:mm:ss a")
243+
244+
val date = formatter.parse(input, algorithm)
245+
assertEquals(1402, date.jalaliYear)
246+
assertEquals(7, date.jalaliMonth)
247+
assertEquals(15, date.jalaliDay)
248+
assertEquals(21, date.hour)
249+
assertEquals(30, date.minute)
250+
assertEquals(0, date.second)
251+
}
252+
253+
@Test
254+
fun testParseNoonBoundary() {
255+
val input = "1402/07/15 12:00:00 PM"
256+
val formatter = JalaliDateTimeFormatter()
257+
.byUnicodePattern("yyyy/MM/dd hh:mm:ss a")
258+
259+
val date = formatter.parse(input, algorithm)
260+
assertEquals(12, date.hour)
261+
}
262+
263+
@Test
264+
fun testParseMidnightBoundary() {
265+
val input = "1402/07/15 12:00:00 AM"
266+
val formatter = JalaliDateTimeFormatter()
267+
.byUnicodePattern("yyyy/MM/dd hh:mm:ss a")
268+
269+
val date = formatter.parse(input, algorithm)
270+
assertEquals(0, date.hour)
271+
}
272+
273+
@Test
274+
fun testParseAmPmWithTextMixPattern() {
275+
val input = "شنبه 1402/01/01 11:15 PM"
276+
val formatter = JalaliDateTimeFormatter()
277+
.byUnicodePattern("EEEE yyyy/MM/dd hh:mm a")
278+
279+
val date = formatter.parse(input, algorithm)
280+
assertEquals(1402, date.jalaliYear)
281+
assertEquals(1, date.jalaliMonth)
282+
assertEquals(1, date.jalaliDay)
283+
assertEquals(23, date.hour)
284+
assertEquals(15, date.minute)
285+
}
222286
}

0 commit comments

Comments
 (0)