@planet-matrix/mobius-model 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +30 -1
  3. package/dist/index.js +4 -2
  4. package/dist/index.js.map +22 -4
  5. package/package.json +3 -3
  6. package/scripts/build.ts +4 -4
  7. package/src/basic/README.md +144 -0
  8. package/src/basic/array.ts +872 -0
  9. package/src/basic/bigint.ts +114 -0
  10. package/src/basic/boolean.ts +180 -0
  11. package/src/basic/enhance.ts +10 -0
  12. package/src/basic/error.ts +51 -0
  13. package/src/basic/function.ts +453 -0
  14. package/src/basic/helper.ts +276 -0
  15. package/src/basic/index.ts +17 -0
  16. package/src/basic/is.ts +320 -0
  17. package/src/basic/number.ts +178 -0
  18. package/src/basic/object.ts +140 -0
  19. package/src/basic/promise.ts +464 -0
  20. package/src/basic/regexp.ts +7 -0
  21. package/src/basic/stream.ts +140 -0
  22. package/src/basic/string.ts +308 -0
  23. package/src/basic/symbol.ts +164 -0
  24. package/src/basic/temporal.ts +224 -0
  25. package/src/encoding/README.md +105 -0
  26. package/src/encoding/base64.ts +98 -0
  27. package/src/encoding/index.ts +1 -0
  28. package/src/index.ts +4 -0
  29. package/src/random/README.md +109 -0
  30. package/src/random/index.ts +1 -0
  31. package/src/random/uuid.ts +103 -0
  32. package/src/type/README.md +330 -0
  33. package/src/type/array.ts +5 -0
  34. package/src/type/boolean.ts +471 -0
  35. package/src/type/class.ts +419 -0
  36. package/src/type/function.ts +1519 -0
  37. package/src/type/helper.ts +135 -0
  38. package/src/type/index.ts +14 -0
  39. package/src/type/intersection.ts +93 -0
  40. package/src/type/is.ts +247 -0
  41. package/src/type/iteration.ts +233 -0
  42. package/src/type/number.ts +732 -0
  43. package/src/type/object.ts +788 -0
  44. package/src/type/path.ts +73 -0
  45. package/src/type/string.ts +1004 -0
  46. package/src/type/tuple.ts +2424 -0
  47. package/src/type/union.ts +108 -0
  48. package/tests/unit/basic/array.spec.ts +290 -0
  49. package/tests/unit/basic/bigint.spec.ts +50 -0
  50. package/tests/unit/basic/boolean.spec.ts +74 -0
  51. package/tests/unit/basic/error.spec.ts +32 -0
  52. package/tests/unit/basic/function.spec.ts +175 -0
  53. package/tests/unit/basic/helper.spec.ts +118 -0
  54. package/tests/unit/basic/number.spec.ts +74 -0
  55. package/tests/unit/basic/object.spec.ts +46 -0
  56. package/tests/unit/basic/promise.spec.ts +232 -0
  57. package/tests/unit/basic/regexp.spec.ts +11 -0
  58. package/tests/unit/basic/stream.spec.ts +120 -0
  59. package/tests/unit/basic/string.spec.ts +74 -0
  60. package/tests/unit/basic/symbol.spec.ts +72 -0
  61. package/tests/unit/basic/temporal.spec.ts +78 -0
  62. package/tests/unit/encoding/base64.spec.ts +40 -0
  63. package/tests/unit/random/uuid.spec.ts +37 -0
  64. package/dist/index.d.ts +0 -2
  65. package/dist/index.d.ts.map +0 -1
  66. package/dist/reactor/index.d.ts +0 -3
  67. package/dist/reactor/index.d.ts.map +0 -1
  68. package/dist/reactor/reactor-core/flags.d.ts +0 -99
  69. package/dist/reactor/reactor-core/flags.d.ts.map +0 -1
  70. package/dist/reactor/reactor-core/index.d.ts +0 -4
  71. package/dist/reactor/reactor-core/index.d.ts.map +0 -1
  72. package/dist/reactor/reactor-core/primitive.d.ts +0 -276
  73. package/dist/reactor/reactor-core/primitive.d.ts.map +0 -1
  74. package/dist/reactor/reactor-core/reactive-system.d.ts +0 -241
  75. package/dist/reactor/reactor-core/reactive-system.d.ts.map +0 -1
  76. package/dist/reactor/reactor-operators/branch.d.ts +0 -19
  77. package/dist/reactor/reactor-operators/branch.d.ts.map +0 -1
  78. package/dist/reactor/reactor-operators/convert.d.ts +0 -30
  79. package/dist/reactor/reactor-operators/convert.d.ts.map +0 -1
  80. package/dist/reactor/reactor-operators/create.d.ts +0 -26
  81. package/dist/reactor/reactor-operators/create.d.ts.map +0 -1
  82. package/dist/reactor/reactor-operators/filter.d.ts +0 -269
  83. package/dist/reactor/reactor-operators/filter.d.ts.map +0 -1
  84. package/dist/reactor/reactor-operators/index.d.ts +0 -8
  85. package/dist/reactor/reactor-operators/index.d.ts.map +0 -1
  86. package/dist/reactor/reactor-operators/join.d.ts +0 -48
  87. package/dist/reactor/reactor-operators/join.d.ts.map +0 -1
  88. package/dist/reactor/reactor-operators/map.d.ts +0 -165
  89. package/dist/reactor/reactor-operators/map.d.ts.map +0 -1
  90. package/dist/reactor/reactor-operators/utility.d.ts +0 -48
  91. package/dist/reactor/reactor-operators/utility.d.ts.map +0 -1
@@ -0,0 +1,308 @@
1
+ const internalRandomPacket: Record<number, string[]> = {}
2
+ /**
3
+ * Generate a random string with a fixed length and optional character set.
4
+ *
5
+ * @example
6
+ * ```
7
+ * // Expect: 8
8
+ * const example1 = stringRandom(8).length
9
+ * // Expect: true
10
+ * const example2 = stringRandom(4, "ab").split("").every(char => char === "a" || char === "b")
11
+ * ```
12
+ */
13
+ export const stringRandom = (length: number, chars?: string | undefined): string => {
14
+ let result = ""
15
+ const preparedChars = chars ?? "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
16
+
17
+ Array.from({ length }).forEach(() => {
18
+ result = result + preparedChars[Math.floor(Math.random() * preparedChars.length)]!
19
+ })
20
+
21
+ internalRandomPacket[length] = internalRandomPacket[length] ?? []
22
+ const packet = internalRandomPacket[length]
23
+ if (packet.includes(result)) {
24
+ return stringRandom(length, preparedChars)
25
+ }
26
+ else {
27
+ packet.push(result)
28
+ return result
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Convert a camelCase string into kebab-case.
34
+ *
35
+ * @example
36
+ * ```
37
+ * // Expect: "hello-world"
38
+ * const example1 = stringCamelCaseToKebabCase("helloWorld")
39
+ * // Expect: "ab2-cd"
40
+ * const example2 = stringCamelCaseToKebabCase("ab2Cd")
41
+ * ```
42
+ */
43
+ export const stringCamelCaseToKebabCase = (camelCase: string): string => {
44
+ return camelCase.replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
45
+ }
46
+
47
+ /**
48
+ * Convert a kebab-case string into camelCase.
49
+ *
50
+ * @example
51
+ * ```
52
+ * // Expect: "helloWorld"
53
+ * const example1 = stringKebabCaseToCamelCase("hello-world")
54
+ * // Expect: "ab2Cd"
55
+ * const example2 = stringKebabCaseToCamelCase("ab2-cd")
56
+ * ```
57
+ */
58
+ export const stringKebabCaseToCamelCase = (kebabCase: string): string => {
59
+ return kebabCase.replaceAll(/-./g, x => x[1]!.toUpperCase())
60
+ }
61
+
62
+ /**
63
+ * Get a greeting based on the current local hour.
64
+ *
65
+ * @example
66
+ * ```
67
+ * // Expect: "你好" | "早上好" | "晚上好" | ...
68
+ * const example1 = stringHelloWord()
69
+ * ```
70
+ */
71
+ export const stringHelloWord = (): string => {
72
+ const currentHour = new Date().getHours()
73
+ if (currentHour < 5) {
74
+ return "凌晨好"
75
+ }
76
+ if (currentHour < 8) {
77
+ return "早上好"
78
+ }
79
+ if (currentHour < 12) {
80
+ return "上午好"
81
+ }
82
+ if (currentHour < 14) {
83
+ return "中午好"
84
+ }
85
+ if (currentHour < 17) {
86
+ return "下午好"
87
+ }
88
+ if (currentHour < 19) {
89
+ return "傍晚好"
90
+ }
91
+ if (currentHour < 24) {
92
+ return "晚上好"
93
+ }
94
+ return "你好"
95
+ }
96
+
97
+ /**
98
+ * Calculate the total unit length of a string.
99
+ *
100
+ * @example
101
+ * ```
102
+ * // Expect: 1.5
103
+ * const example1 = stringCalculateUnits("a中")
104
+ * // Expect: 2
105
+ * const example2 = stringCalculateUnits("中文")
106
+ * ```
107
+ */
108
+ export const stringCalculateUnits = (text: string): number => {
109
+ let units = 0
110
+
111
+ for (const char of text) {
112
+ if (/[\u0000-\u007F]/.test(char)) {
113
+ // Half-width character (e.g., ASCII)
114
+ units = units + 0.5
115
+ }
116
+ else {
117
+ // Full-width character (e.g., CJK)
118
+ units = units + 1
119
+ }
120
+ }
121
+
122
+ return units
123
+ }
124
+
125
+ /**
126
+ * Truncate a string by the unit limit and keep whole characters.
127
+ *
128
+ * @example
129
+ * ```
130
+ * // Expect: "a中"
131
+ * const example1 = stringTruncateByUnits("a中文", 1.5)
132
+ * // Expect: "abc"
133
+ * const example2 = stringTruncateByUnits("abc", 2)
134
+ * ```
135
+ */
136
+ export const stringTruncateByUnits = (text: string, maxUnits: number): string => {
137
+ let truncated = ""
138
+ let units = 0
139
+
140
+ for (const char of text) {
141
+ const charUnits = /[\u0000-\u007F]/.test(char) ? 0.5 : 1
142
+ if (units + charUnits > maxUnits) { break }
143
+
144
+ truncated = truncated + char
145
+ units = units + charUnits
146
+ }
147
+
148
+ return truncated
149
+ }
150
+
151
+ /**
152
+ * Slice a string by unit indices.
153
+ *
154
+ * @example
155
+ * ```
156
+ * // Expect: "a中"
157
+ * const example1 = stringSliceByUnits("a中文", 0, 1.5)
158
+ * // Expect: "文"
159
+ * const example2 = stringSliceByUnits("中文ab", 1, 2)
160
+ * ```
161
+ */
162
+ export const stringSliceByUnits = (text: string, start: number, end: number): string => {
163
+ let units = 0
164
+ let result = ""
165
+ for (const char of text) {
166
+ const charUnits = /[\u0000-\u007F]/.test(char) ? 0.5 : 1
167
+ if (units >= end) { break }
168
+
169
+ if (units >= start) {
170
+ result = result + char
171
+ }
172
+ units = units + charUnits
173
+ }
174
+
175
+ return result
176
+ }
177
+
178
+ export interface StringSplitOptions {
179
+ input: string
180
+ chunkSize: number
181
+ chunkOverlap: number
182
+ }
183
+
184
+ /**
185
+ * Split a long string into fixed-size chunks with optional overlap.
186
+ *
187
+ * @example
188
+ * ```
189
+ * // Expect: ["hello", "lo wo", "world"]
190
+ * const example1 = stringSplit({ input: "hello world", chunkSize: 5, chunkOverlap: 2 })
191
+ * ```
192
+ */
193
+ export const stringSplit = (options: StringSplitOptions): string[] => {
194
+ const { input, chunkSize = 1_500, chunkOverlap: overlap = 20 } = options
195
+ const safeChunkSize = Math.max(1, Math.trunc(chunkSize))
196
+ const safeOverlap = Math.max(0, Math.min(Math.trunc(overlap), safeChunkSize - 1))
197
+
198
+ // 去除换行符和其他特殊符号
199
+ const sanitizedInput = input.replaceAll(/[\t\n\r]/g, " ")
200
+
201
+ // 分割结果数组
202
+ const result: string[] = []
203
+
204
+ // 当前处理位置
205
+ let currentPosition = 0
206
+
207
+ // 循环处理整个输入字符串
208
+ while (currentPosition < sanitizedInput.length) {
209
+ // 计算当前片段的结束位置
210
+ const endPosition = Math.min(currentPosition + safeChunkSize, sanitizedInput.length)
211
+
212
+ // 获取当前片段
213
+ const segment = sanitizedInput.slice(currentPosition, endPosition)
214
+
215
+ // 将当前片段添加到结果数组中
216
+ result.push(segment)
217
+
218
+ // 如果已经到达文本末尾,退出循环
219
+ if (endPosition >= sanitizedInput.length) {
220
+ break
221
+ }
222
+
223
+ // 更新处理位置,有重叠部分
224
+ currentPosition = endPosition - safeOverlap
225
+ }
226
+
227
+ return result
228
+ }
229
+
230
+ /**
231
+ * Split a string into chunks while trying to keep natural line breaks.
232
+ *
233
+ * @example
234
+ * ```
235
+ * // Expect: ["a\\n", "b\\n", "c"]
236
+ * const example1 = stringSmartSplit("a\nb\nc", 2)
237
+ * ```
238
+ */
239
+ export const stringSmartSplit = (text: string, maxLength: number): string[] => {
240
+ let _text = text
241
+ if (_text.length <= maxLength) {
242
+ return [_text]
243
+ }
244
+
245
+ const conservativeMaxLength = maxLength * 0.9
246
+
247
+ const threshold = maxLength * 0.2
248
+ const texts: string[] = []
249
+ while (_text.length > conservativeMaxLength) {
250
+ texts.push(_text.slice(0, conservativeMaxLength))
251
+ _text = _text.slice(conservativeMaxLength)
252
+ }
253
+ if (_text.length > threshold) {
254
+ texts.push(_text)
255
+ }
256
+ else {
257
+ texts[texts.length - 1] = texts.at(-1)! + _text
258
+ }
259
+
260
+ if (texts.length === 1) {
261
+ return texts
262
+ }
263
+
264
+ for (let i = 1; i < texts.length; i = i + 1) {
265
+ const previousText = texts[i - 1]!
266
+ const currentText = texts[i]!
267
+ const lastIndexOfNewLineOfPreviousText = previousText.lastIndexOf("\n")
268
+ const firstIndexOfNewLineOfCurrentText = currentText.indexOf("\n")
269
+ const endOfPreviousText = previousText.slice(lastIndexOfNewLineOfPreviousText + 1)
270
+ const startOfCurrentText = currentText.slice(0, firstIndexOfNewLineOfCurrentText)
271
+ if (endOfPreviousText.length >= startOfCurrentText.length) {
272
+ const newPreviousText = previousText + startOfCurrentText
273
+ const newCurrentText = currentText.slice(firstIndexOfNewLineOfCurrentText + 1)
274
+ texts[i - 1] = newPreviousText
275
+ texts[i] = newCurrentText
276
+ }
277
+ else {
278
+ const newPreviousText = previousText.slice(0, lastIndexOfNewLineOfPreviousText)
279
+ const newCurrentText = endOfPreviousText + currentText
280
+ texts[i - 1] = newPreviousText
281
+ texts[i] = newCurrentText
282
+ }
283
+ }
284
+
285
+ const trimmedTexts = texts.map(text => text.trim())
286
+
287
+ return trimmedTexts
288
+ }
289
+
290
+ /**
291
+ * Truncate a string to a maximum length with an ellipsis.
292
+ *
293
+ * @example
294
+ * ```
295
+ * // Expect: "hello..."
296
+ * const example1 = stringTruncate("hello world", 5)
297
+ * // Expect: "hi"
298
+ * const example2 = stringTruncate("hi", 5)
299
+ * ```
300
+ */
301
+ export const stringTruncate = (str: string, n: number): string => {
302
+ if (str.length <= n) {
303
+ return str
304
+ }
305
+ else {
306
+ return `${str.slice(0, n)}...`
307
+ }
308
+ }
@@ -0,0 +1,164 @@
1
+ import { isSymbol } from "./is.ts"
2
+
3
+ /**
4
+ * Create a new unique symbol with an optional description.
5
+ *
6
+ * @example
7
+ * ```
8
+ * // Expect: true
9
+ * const example1 = typeof symbolCreateLocal("demo") === "symbol"
10
+ * // Expect: false
11
+ * const example2 = symbolCreateLocal("demo") === symbolCreateLocal("demo")
12
+ * ```
13
+ */
14
+ export const symbolCreateLocal = (description?: string): symbol => {
15
+ return Symbol(description)
16
+ }
17
+
18
+ /**
19
+ * Get or create a global symbol for the given key.
20
+ *
21
+ * @example
22
+ * ```
23
+ * // Expect: true
24
+ * const example1 = symbolCreateGlobal("demo") === Symbol.for("demo")
25
+ * // Expect: true
26
+ * const example2 = symbolCreateGlobal("demo") === symbolCreateGlobal("demo")
27
+ * ```
28
+ */
29
+ export const symbolCreateGlobal = (key: string): symbol => Symbol.for(key)
30
+
31
+ /**
32
+ * Get the key for a global symbol, or undefined if not global.
33
+ *
34
+ * @example
35
+ * ```
36
+ * // Expect: "demo"
37
+ * const example1 = symbolGetKey(Symbol.for("demo"))
38
+ * // Expect: undefined
39
+ * const example2 = symbolGetKey(Symbol("demo"))
40
+ * ```
41
+ */
42
+ export const symbolGetKey = (value: unknown): string | undefined => {
43
+ return isSymbol(value) ? Symbol.keyFor(value) : undefined
44
+ }
45
+
46
+ /**
47
+ * Check whether a symbol is from the global registry.
48
+ *
49
+ * @example
50
+ * ```
51
+ * // Expect: true
52
+ * const example1 = symbolIsGlobal(Symbol.for("demo"))
53
+ * // Expect: false
54
+ * const example2 = symbolIsGlobal(Symbol("demo"))
55
+ * ```
56
+ */
57
+ export const symbolIsGlobal = (value: unknown): value is symbol => {
58
+ return isSymbol(value) && Symbol.keyFor(value) !== undefined
59
+ }
60
+
61
+ /**
62
+ * Check whether a symbol is not from the global registry.
63
+ *
64
+ * @example
65
+ * ```
66
+ * // Expect: false
67
+ * const example1 = symbolIsLocal(Symbol.for("demo"))
68
+ * // Expect: true
69
+ * const example2 = symbolIsLocal(Symbol("demo"))
70
+ * ```
71
+ */
72
+ export const symbolIsLocal = (value: unknown): value is symbol => {
73
+ return isSymbol(value) && Symbol.keyFor(value) === undefined
74
+ }
75
+
76
+ /**
77
+ * Check whether a symbol has a description.
78
+ *
79
+ * @example
80
+ * ```
81
+ * // Expect: true
82
+ * const example1 = symbolHasDescription(Symbol("demo"))
83
+ * // Expect: false
84
+ * const example2 = symbolHasDescription(Symbol())
85
+ * ```
86
+ */
87
+ export const symbolHasDescription = (value: unknown): value is symbol => {
88
+ return isSymbol(value) && value.description !== undefined
89
+ }
90
+
91
+ /**
92
+ * Get the description of a symbol, or undefined for non-symbols.
93
+ *
94
+ * @example
95
+ * ```
96
+ * // Expect: "demo"
97
+ * const example1 = symbolGetDescription(Symbol("demo"))
98
+ * // Expect: undefined
99
+ * const example2 = symbolGetDescription(Symbol())
100
+ * ```
101
+ */
102
+ export const symbolGetDescription = (value: unknown): string | undefined => {
103
+ return isSymbol(value) ? value.description : undefined
104
+ }
105
+
106
+ /**
107
+ * Check whether a symbol is anonymous (no description).
108
+ *
109
+ * @example
110
+ * ```
111
+ * // Expect: false
112
+ * const example1 = symbolIsAnonymous(Symbol("demo"))
113
+ * // Expect: true
114
+ * const example2 = symbolIsAnonymous(Symbol())
115
+ * ```
116
+ */
117
+ export const symbolIsAnonymous = (value: unknown): value is symbol => {
118
+ return isSymbol(value) && value.description === undefined
119
+ }
120
+
121
+ const internalWellKnownSymbols = new Set<symbol>([
122
+ Symbol.asyncIterator,
123
+ Symbol.hasInstance,
124
+ Symbol.isConcatSpreadable,
125
+ Symbol.iterator,
126
+ Symbol.match,
127
+ Symbol.matchAll,
128
+ Symbol.replace,
129
+ Symbol.search,
130
+ Symbol.species,
131
+ Symbol.split,
132
+ Symbol.toPrimitive,
133
+ Symbol.toStringTag,
134
+ Symbol.unscopables,
135
+ ])
136
+ /**
137
+ * Check whether a symbol is one of the well-known symbols.
138
+ *
139
+ * @example
140
+ * ```
141
+ * // Expect: true
142
+ * const example1 = symbolIsWellKnown(Symbol.iterator)
143
+ * // Expect: false
144
+ * const example2 = symbolIsWellKnown(Symbol("iterator"))
145
+ * ```
146
+ */
147
+ export const symbolIsWellKnown = (value: unknown): value is symbol => {
148
+ return isSymbol(value) && internalWellKnownSymbols.has(value)
149
+ }
150
+
151
+ /**
152
+ * Convert a symbol to its string representation.
153
+ *
154
+ * @example
155
+ * ```
156
+ * // Expect: "Symbol(demo)"
157
+ * const example1 = symbolToString(Symbol("demo"))
158
+ * // Expect: undefined
159
+ * const example2 = symbolToString("demo")
160
+ * ```
161
+ */
162
+ export const symbolToString = (value: unknown): string | undefined => {
163
+ return isSymbol(value) ? value.toString() : undefined
164
+ }
@@ -0,0 +1,224 @@
1
+ import { isDate } from "./is.ts"
2
+
3
+ /**
4
+ * Check whether a date is outdated (in the past).
5
+ */
6
+ export const temporalIsOutdated = (target: unknown): boolean => {
7
+ return isDate(target) && (new Date(target).getTime() < Date.now())
8
+ }
9
+
10
+ /**
11
+ * Format a timestamp to a YYYY-MM-DD string.
12
+ *
13
+ * @example
14
+ * ```
15
+ * // Expect: "1970-01-01"
16
+ * const example1 = temporalFormatToYYYYMMDD(0)
17
+ * // Expect: "2000/01/02"
18
+ * const example2 = temporalFormatToYYYYMMDD(946771200000, "/")
19
+ * ```
20
+ */
21
+ export const temporalFormatToYYYYMMDD = (timestamp: number, separator = "-"): string => {
22
+ const date = new Date(timestamp)
23
+ const year = date.getFullYear()
24
+ const month = String(date.getMonth() + 1).padStart(2, "0")
25
+ const day = String(date.getDate()).padStart(2, "0")
26
+ return `${year}${separator}${month}${separator}${day}`
27
+ }
28
+
29
+ /**
30
+ * Format a timestamp to a hh:mm:ss string.
31
+ *
32
+ * @example
33
+ * ```
34
+ * // Expect: "00:00:00"
35
+ * const example1 = temporalFormatTohhmmss(0)
36
+ * // Expect: "12:34:56"
37
+ * const example2 = temporalFormatTohhmmss(45296000)
38
+ * ```
39
+ */
40
+ export const temporalFormatTohhmmss = (timestamp: number, separator = ":"): string => {
41
+ const date = new Date(timestamp)
42
+ const hours = String(date.getHours()).padStart(2, "0")
43
+ const minutes = String(date.getMinutes()).padStart(2, "0")
44
+ const seconds = String(date.getSeconds()).padStart(2, "0")
45
+ return `${hours}${separator}${minutes}${separator}${seconds}`
46
+ }
47
+
48
+ /**
49
+ * Format a timestamp to a YYYY-MM-DD hh:mm:ss string.
50
+ *
51
+ * @example
52
+ * ```
53
+ * // Expect: "1970-01-01 00:00:00"
54
+ * const example1 = temporalFormatToYYYYMMDDhhmmss(0)
55
+ * // Expect: "2000-01-02 00:00:00"
56
+ * const example2 = temporalFormatToYYYYMMDDhhmmss(946771200000)
57
+ * ```
58
+ */
59
+ export const temporalFormatToYYYYMMDDhhmmss = (timestamp: number): string => {
60
+ return `${temporalFormatToYYYYMMDD(timestamp)} ${temporalFormatTohhmmss(timestamp)}`
61
+ }
62
+
63
+ /**
64
+ * Format a timestamp to a relative time string (e.g., "刚刚", "5 分钟前").
65
+ *
66
+ * @example
67
+ * ```
68
+ * // Expect: "刚刚"
69
+ * const originalNow = Date.now
70
+ * Date.now = () => 3_600_000
71
+ * const example1 = temporalFormatToRelativeTime(3_600_000)
72
+ * // Expect: "1 小时前"
73
+ * const example2 = temporalFormatToRelativeTime(0)
74
+ * Date.now = originalNow
75
+ * ```
76
+ */
77
+ export const temporalFormatToRelativeTime = (timestamp: number): string => {
78
+ const now = Date.now()
79
+ const diffInSeconds = Math.floor((now - timestamp) / 1_000)
80
+
81
+ if (diffInSeconds < 60) {
82
+ return "刚刚"
83
+ }
84
+ else if (diffInSeconds < 3_600) {
85
+ const minutes = Math.floor(diffInSeconds / 60)
86
+ return `${minutes} 分钟前`
87
+ }
88
+ else if (diffInSeconds < 86_400) {
89
+ const hours = Math.floor(diffInSeconds / 3_600)
90
+ return `${hours} 小时前`
91
+ }
92
+ else if (diffInSeconds < 172_800) {
93
+ return "昨天"
94
+ }
95
+ else if (diffInSeconds < 604_800) {
96
+ return "一周内"
97
+ }
98
+ else {
99
+ // 格式化为 YYYY-MM-DD hh:ss
100
+ return temporalFormatToYYYYMMDDhhmmss(timestamp)
101
+ }
102
+ }
103
+
104
+ export interface TemporalFormatToWechatRelativeTimeOptions {
105
+ timestamp: number
106
+ alwaysShowTime?: boolean | undefined
107
+ }
108
+ /**
109
+ * Format a timestamp to a WeChat-style relative time string.
110
+ *
111
+ * @example
112
+ * ```
113
+ * // Expect: "70/01/01"
114
+ * const example1 = temporalFormatToWechatRelativeTime({ timestamp: 0 })
115
+ * // Expect: "70/01/01 00:00"
116
+ * const example2 = temporalFormatToWechatRelativeTime({ timestamp: 0, alwaysShowTime: true })
117
+ * ```
118
+ */
119
+ export const temporalFormatToWechatRelativeTime = (
120
+ options: TemporalFormatToWechatRelativeTimeOptions
121
+ ): string => {
122
+ const {
123
+ timestamp,
124
+ alwaysShowTime = false,
125
+ } = options
126
+
127
+ const now = new Date()
128
+ const targetDate = new Date(timestamp)
129
+
130
+ const padZero = (num: number): string => num.toString().padStart(2, "0")
131
+
132
+ const isSameDay = (date1: Date, date2: Date): boolean => {
133
+ return date1.getFullYear() === date2.getFullYear()
134
+ && date1.getMonth() === date2.getMonth()
135
+ && date1.getDate() === date2.getDate()
136
+ }
137
+
138
+ const isYesterday = (date1: Date, date2: Date): boolean => {
139
+ const yesterday = new Date(date1)
140
+ yesterday.setDate(date1.getDate() - 1)
141
+ return isSameDay(yesterday, date2)
142
+ }
143
+
144
+ const getWeekDayName = (date: Date): string => {
145
+ const weekdays = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"]
146
+ return weekdays[date.getDay()]!
147
+ }
148
+
149
+ const isSameWeek = (date1: Date, date2: Date): boolean => {
150
+ const startOfWeek = (date: Date): Date => {
151
+ const result = new Date(date)
152
+ const day = result.getDay()
153
+ const diff = result.getDate() - day + (day === 0 ? -6 : 1)
154
+ result.setDate(diff)
155
+ return new Date(result.getFullYear(), result.getMonth(), result.getDate())
156
+ }
157
+
158
+ return startOfWeek(date1).getTime() === startOfWeek(date2).getTime()
159
+ }
160
+
161
+ const date = `${padZero(targetDate.getFullYear() % 100)}/${padZero(targetDate.getMonth() + 1)}/${padZero(targetDate.getDate())}`
162
+ const time = `${padZero(targetDate.getHours())}:${padZero(targetDate.getMinutes())}`
163
+
164
+ if (isSameDay(now, targetDate)) {
165
+ return time
166
+ }
167
+ else if (isYesterday(now, targetDate)) {
168
+ return alwaysShowTime ? `昨天 ${time}` : "昨天"
169
+ }
170
+ else if (isSameWeek(now, targetDate)) {
171
+ return alwaysShowTime ? `${getWeekDayName(targetDate)} ${time}` : getWeekDayName(targetDate)
172
+ }
173
+ else {
174
+ return alwaysShowTime ? `${date} ${time}` : date
175
+ }
176
+ }
177
+
178
+ type InternalUnitsDict = Record<string, string>
179
+ const INTERNAL_UNITS_DICT: InternalUnitsDict = {
180
+ year: "年",
181
+ month: "月",
182
+ week: "周",
183
+ day: "天",
184
+ hour: "小时",
185
+ minute: "分钟",
186
+ second: "秒",
187
+ millisecond: "毫秒",
188
+ }
189
+ type InternalUnitsMilliDict = Record<string, number>
190
+ const INTERNAL_UNITS_MILLI_DICT: InternalUnitsMilliDict = {
191
+ year: 31_557_600_000,
192
+ month: 2_629_800_000,
193
+ week: 604_800_000,
194
+ day: 86_400_000,
195
+ hour: 3_600_000,
196
+ minute: 60_000,
197
+ second: 1_000,
198
+ millisecond: 1,
199
+ }
200
+ /**
201
+ * Convert a timestamp to a human-readable relative time string in Chinese.
202
+ *
203
+ * @example
204
+ * ```
205
+ * // Expect: "刚刚"
206
+ * const originalNow = Date.now
207
+ * Date.now = () => 3_600_000
208
+ * const example1 = temporalHumanize(3_600_000)
209
+ * // Expect: "1 小时前"
210
+ * const example2 = temporalHumanize(0)
211
+ * Date.now = originalNow
212
+ * ```
213
+ */
214
+ export const temporalHumanize = (timestamp: number): string => {
215
+ let res = ""
216
+ const milliseconds = Date.now() - timestamp
217
+ for (const key in INTERNAL_UNITS_MILLI_DICT) {
218
+ if (milliseconds >= INTERNAL_UNITS_MILLI_DICT[key]!) {
219
+ res = `${Math.floor(milliseconds / INTERNAL_UNITS_MILLI_DICT[key]!)} ${INTERNAL_UNITS_DICT[key]}前`
220
+ break
221
+ }
222
+ }
223
+ return res !== "" ? res : "刚刚"
224
+ }