@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.
- package/CHANGELOG.md +15 -0
- package/README.md +30 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +22 -4
- package/package.json +3 -3
- package/scripts/build.ts +4 -4
- package/src/basic/README.md +144 -0
- package/src/basic/array.ts +872 -0
- package/src/basic/bigint.ts +114 -0
- package/src/basic/boolean.ts +180 -0
- package/src/basic/enhance.ts +10 -0
- package/src/basic/error.ts +51 -0
- package/src/basic/function.ts +453 -0
- package/src/basic/helper.ts +276 -0
- package/src/basic/index.ts +17 -0
- package/src/basic/is.ts +320 -0
- package/src/basic/number.ts +178 -0
- package/src/basic/object.ts +140 -0
- package/src/basic/promise.ts +464 -0
- package/src/basic/regexp.ts +7 -0
- package/src/basic/stream.ts +140 -0
- package/src/basic/string.ts +308 -0
- package/src/basic/symbol.ts +164 -0
- package/src/basic/temporal.ts +224 -0
- package/src/encoding/README.md +105 -0
- package/src/encoding/base64.ts +98 -0
- package/src/encoding/index.ts +1 -0
- package/src/index.ts +4 -0
- package/src/random/README.md +109 -0
- package/src/random/index.ts +1 -0
- package/src/random/uuid.ts +103 -0
- package/src/type/README.md +330 -0
- package/src/type/array.ts +5 -0
- package/src/type/boolean.ts +471 -0
- package/src/type/class.ts +419 -0
- package/src/type/function.ts +1519 -0
- package/src/type/helper.ts +135 -0
- package/src/type/index.ts +14 -0
- package/src/type/intersection.ts +93 -0
- package/src/type/is.ts +247 -0
- package/src/type/iteration.ts +233 -0
- package/src/type/number.ts +732 -0
- package/src/type/object.ts +788 -0
- package/src/type/path.ts +73 -0
- package/src/type/string.ts +1004 -0
- package/src/type/tuple.ts +2424 -0
- package/src/type/union.ts +108 -0
- package/tests/unit/basic/array.spec.ts +290 -0
- package/tests/unit/basic/bigint.spec.ts +50 -0
- package/tests/unit/basic/boolean.spec.ts +74 -0
- package/tests/unit/basic/error.spec.ts +32 -0
- package/tests/unit/basic/function.spec.ts +175 -0
- package/tests/unit/basic/helper.spec.ts +118 -0
- package/tests/unit/basic/number.spec.ts +74 -0
- package/tests/unit/basic/object.spec.ts +46 -0
- package/tests/unit/basic/promise.spec.ts +232 -0
- package/tests/unit/basic/regexp.spec.ts +11 -0
- package/tests/unit/basic/stream.spec.ts +120 -0
- package/tests/unit/basic/string.spec.ts +74 -0
- package/tests/unit/basic/symbol.spec.ts +72 -0
- package/tests/unit/basic/temporal.spec.ts +78 -0
- package/tests/unit/encoding/base64.spec.ts +40 -0
- package/tests/unit/random/uuid.spec.ts +37 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/reactor/index.d.ts +0 -3
- package/dist/reactor/index.d.ts.map +0 -1
- package/dist/reactor/reactor-core/flags.d.ts +0 -99
- package/dist/reactor/reactor-core/flags.d.ts.map +0 -1
- package/dist/reactor/reactor-core/index.d.ts +0 -4
- package/dist/reactor/reactor-core/index.d.ts.map +0 -1
- package/dist/reactor/reactor-core/primitive.d.ts +0 -276
- package/dist/reactor/reactor-core/primitive.d.ts.map +0 -1
- package/dist/reactor/reactor-core/reactive-system.d.ts +0 -241
- package/dist/reactor/reactor-core/reactive-system.d.ts.map +0 -1
- package/dist/reactor/reactor-operators/branch.d.ts +0 -19
- package/dist/reactor/reactor-operators/branch.d.ts.map +0 -1
- package/dist/reactor/reactor-operators/convert.d.ts +0 -30
- package/dist/reactor/reactor-operators/convert.d.ts.map +0 -1
- package/dist/reactor/reactor-operators/create.d.ts +0 -26
- package/dist/reactor/reactor-operators/create.d.ts.map +0 -1
- package/dist/reactor/reactor-operators/filter.d.ts +0 -269
- package/dist/reactor/reactor-operators/filter.d.ts.map +0 -1
- package/dist/reactor/reactor-operators/index.d.ts +0 -8
- package/dist/reactor/reactor-operators/index.d.ts.map +0 -1
- package/dist/reactor/reactor-operators/join.d.ts +0 -48
- package/dist/reactor/reactor-operators/join.d.ts.map +0 -1
- package/dist/reactor/reactor-operators/map.d.ts +0 -165
- package/dist/reactor/reactor-operators/map.d.ts.map +0 -1
- package/dist/reactor/reactor-operators/utility.d.ts +0 -48
- 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
|
+
}
|