Skip to content

Commit 64d51b9

Browse files
committed
feat(format): add new format tokens and helper function 7️⃣
- Add Do format token for ordinal day - Add tests for new format tokens - Add toOrdinal helper function - Add X format token for Unix timestamp in seconds - Add x format token for Unix timestamp in milliseconds - Add Z format token for timezone offset with colon - Add ZZ format token for timezone offset without colon - Improve single-character token handling in format
1 parent 28f1650 commit 64d51b9

3 files changed

Lines changed: 391 additions & 24 deletions

File tree

src/helpers/Utilities.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,27 @@
77
export function padNumber(num: number, length: number): string {
88
return num.toString().padStart(length, '0')
99
}
10+
11+
/**
12+
* Converts a number to its ordinal form (1st, 2nd, 3rd, etc.).
13+
* @param num - The number to convert to ordinal
14+
* @returns The ordinal string representation
15+
*/
16+
export function toOrdinal(num: number): string {
17+
const numStr = num.toString()
18+
const lastDigit = numStr[numStr.length - 1]
19+
const lastTwoDigits = numStr.slice(-2)
20+
if (lastTwoDigits === '11' || lastTwoDigits === '12' || lastTwoDigits === '13') {
21+
return `${numStr}th`
22+
}
23+
if (lastDigit === '1') {
24+
return `${numStr}st`
25+
}
26+
if (lastDigit === '2') {
27+
return `${numStr}nd`
28+
}
29+
if (lastDigit === '3') {
30+
return `${numStr}rd`
31+
}
32+
return `${numStr}th`
33+
}

src/utils/Format.ts

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,32 +23,55 @@ export function formatDate(date: Date, pattern: string, localeCode?: string): st
2323
const dayNameShort = locale.dayNamesShort[dayOfWeek] ?? 'Unknown'
2424
const monthName = locale.monthNames[month - 1] ?? 'Unknown'
2525
const monthNameShort = locale.monthNamesShort[month - 1] ?? 'Unknown'
26+
const offsetMinutes = Helpers.getTimezoneOffset(date)
27+
const offsetHours = Math.floor(Math.abs(offsetMinutes) / 60)
28+
const offsetMins = Math.abs(offsetMinutes) % 60
29+
const offsetSign = offsetMinutes >= 0 ? '+' : '-'
30+
const offsetString = `${offsetSign}${Helpers.padNumber(offsetHours, 2)}${
31+
Helpers.padNumber(
32+
offsetMins,
33+
2
34+
)
35+
}`
36+
const offsetStringColon = `${offsetSign}${Helpers.padNumber(offsetHours, 2)}:${
37+
Helpers.padNumber(
38+
offsetMins,
39+
2
40+
)
41+
}`
42+
const unixSeconds = Math.floor(date.getTime() / 1000)
43+
const unixMilliseconds = date.getTime()
2644
const tokens: Record<string, string> = {
27-
YYYY: year.toString(),
28-
YY: (year % 100).toString().padStart(2, '0'),
29-
MM: Helpers.padNumber(month, 2),
30-
M: month.toString(),
31-
DD: Helpers.padNumber(day, 2),
45+
A: hour >= 12 ? 'PM' : 'AM',
3246
D: day.toString(),
33-
HH: Helpers.padNumber(hour, 2),
47+
DD: Helpers.padNumber(day, 2),
48+
Do: Helpers.toOrdinal(day),
3449
H: hour.toString(),
35-
hh: Helpers.padNumber(hour % 12 || 12, 2),
50+
HH: Helpers.padNumber(hour, 2),
51+
M: month.toString(),
52+
MM: Helpers.padNumber(month, 2),
53+
MMM: monthNameShort,
54+
MMMM: monthName,
55+
S: Math.floor(millisecond / 100).toString(),
56+
SS: Helpers.padNumber(Math.floor(millisecond / 10), 2),
57+
SSS: Helpers.padNumber(millisecond, 3),
58+
X: unixSeconds.toString(),
59+
YY: (year % 100).toString().padStart(2, '0'),
60+
YYYY: year.toString(),
61+
Z: offsetStringColon,
62+
ZZ: offsetString,
63+
a: hour >= 12 ? 'pm' : 'am',
64+
d: dayOfWeek.toString(),
65+
dd: Helpers.padNumber(dayOfWeek, 2),
66+
ddd: dayNameShort,
67+
dddd: dayName,
3668
h: (hour % 12 || 12).toString(),
37-
mm: Helpers.padNumber(minute, 2),
69+
hh: Helpers.padNumber(hour % 12 || 12, 2),
3870
m: minute.toString(),
39-
ss: Helpers.padNumber(second, 2),
71+
mm: Helpers.padNumber(minute, 2),
4072
s: second.toString(),
41-
SSS: Helpers.padNumber(millisecond, 3),
42-
SS: Helpers.padNumber(Math.floor(millisecond / 10), 2),
43-
S: Math.floor(millisecond / 100).toString(),
44-
dddd: dayName,
45-
ddd: dayNameShort,
46-
dd: Helpers.padNumber(dayOfWeek, 2),
47-
d: dayOfWeek.toString(),
48-
MMMM: monthName,
49-
MMM: monthNameShort,
50-
A: hour >= 12 ? 'PM' : 'AM',
51-
a: hour >= 12 ? 'pm' : 'am'
73+
ss: Helpers.padNumber(second, 2),
74+
x: unixMilliseconds.toString()
5275
}
5376
const placeholderStart = '\uE000'
5477
let result = pattern
@@ -60,10 +83,39 @@ export function formatDate(date: Date, pattern: string, localeCode?: string): st
6083
continue
6184
}
6285
const value = tokens[key]
63-
if (value !== undefined && result.includes(key)) {
64-
const placeholder = `${placeholderStart}${String.fromCharCode(0xe000 + i)}`
65-
replacements.push({ placeholder, value })
66-
result = result.replaceAll(key, placeholder)
86+
if (value === undefined) {
87+
continue
88+
}
89+
if (key.length === 1) {
90+
const char = key[0]
91+
if (char === undefined) {
92+
continue
93+
}
94+
let searchStart = 0
95+
while (true) {
96+
const index = result.indexOf(char, searchStart)
97+
if (index === -1) {
98+
break
99+
}
100+
const prevChar = index > 0 ? result[index - 1] : ''
101+
const nextChar = index < result.length - 1 ? result[index + 1] : ''
102+
const isPrevAlpha = /[a-zA-Z]/.test(prevChar ?? '')
103+
const isNextAlpha = /[a-zA-Z]/.test(nextChar ?? '')
104+
if (!isPrevAlpha && !isNextAlpha) {
105+
const placeholder = `${placeholderStart}${String.fromCharCode(0xe000 + i)}`
106+
replacements.push({ placeholder, value })
107+
result = result.substring(0, index) + placeholder + result.substring(index + 1)
108+
searchStart = index + placeholder.length
109+
} else {
110+
searchStart = index + 1
111+
}
112+
}
113+
} else {
114+
if (result.includes(key)) {
115+
const placeholder = `${placeholderStart}${String.fromCharCode(0xe000 + i)}`
116+
replacements.push({ placeholder, value })
117+
result = result.replaceAll(key, placeholder)
118+
}
67119
}
68120
}
69121
for (const { placeholder, value } of replacements) {

0 commit comments

Comments
 (0)