@cloudcome/utils-core 0.0.0 → 1.1.1

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 (107) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +21 -0
  3. package/package.json +277 -6
  4. package/src/array.ts +312 -0
  5. package/src/async.ts +379 -0
  6. package/src/base64.ts +20 -0
  7. package/src/cache.ts +146 -0
  8. package/src/color/contrast.ts +20 -0
  9. package/src/color/distance.ts +28 -0
  10. package/src/color/helpers.ts +23 -0
  11. package/src/color/hex-hsl.ts +11 -0
  12. package/src/color/hex-hsv.ts +28 -0
  13. package/src/color/hex-hwb.ts +31 -0
  14. package/src/color/hex-rgb.ts +39 -0
  15. package/src/color/hsl-lighten.ts +15 -0
  16. package/src/color/hsv-brighten.ts +17 -0
  17. package/src/color/luminance.ts +17 -0
  18. package/src/color/mix.ts +26 -0
  19. package/src/color/rgb-hsl.ts +53 -0
  20. package/src/color/rgb-hsv.ts +52 -0
  21. package/src/color/rgb-hwb.ts +56 -0
  22. package/src/color/rgb-lab.ts +33 -0
  23. package/src/color/rgb-whiter.ts +22 -0
  24. package/src/color/rgb-xyz.ts +62 -0
  25. package/src/color/types.ts +65 -0
  26. package/src/color/xyz-lab.ts +54 -0
  27. package/src/color.ts +19 -0
  28. package/src/crypto/md5.mjs +357 -0
  29. package/src/crypto/sha1.mjs +300 -0
  30. package/src/crypto/sha256.mjs +310 -0
  31. package/src/crypto/sha512.mjs +459 -0
  32. package/src/crypto.ts +60 -0
  33. package/src/date/const.ts +6 -0
  34. package/src/date/core.ts +162 -0
  35. package/src/date/days.ts +51 -0
  36. package/src/date/is.ts +186 -0
  37. package/src/date/relative.ts +92 -0
  38. package/src/date/start-end.ts +246 -0
  39. package/src/date/timezone.ts +220 -0
  40. package/src/date/weeks.ts +100 -0
  41. package/src/date.ts +8 -0
  42. package/src/dict.ts +1 -0
  43. package/src/dts/global.d.ts +27 -0
  44. package/src/easing.ts +166 -0
  45. package/src/emitter.ts +117 -0
  46. package/src/enum.ts +171 -0
  47. package/src/env.ts +62 -0
  48. package/src/error.ts +31 -0
  49. package/src/exception.ts +68 -0
  50. package/src/fn.ts +197 -0
  51. package/src/index.ts +1 -0
  52. package/src/number.ts +236 -0
  53. package/src/object/each.ts +56 -0
  54. package/src/object/get-set.ts +273 -0
  55. package/src/object/is.ts +128 -0
  56. package/src/object/merge.ts +180 -0
  57. package/src/object/process.ts +80 -0
  58. package/src/object.ts +5 -0
  59. package/src/path.ts +188 -0
  60. package/src/promise.ts +111 -0
  61. package/src/qs.ts +119 -0
  62. package/src/regexp.ts +156 -0
  63. package/src/string.ts +146 -0
  64. package/src/time/from.ts +57 -0
  65. package/src/time/to.ts +106 -0
  66. package/src/time.ts +2 -0
  67. package/src/timer.ts +226 -0
  68. package/src/tree.ts +394 -0
  69. package/src/type.ts +197 -0
  70. package/src/types.ts +78 -0
  71. package/src/unique.ts +77 -0
  72. package/src/url.ts +93 -0
  73. package/src/version.ts +71 -0
  74. package/test/array.test.ts +332 -0
  75. package/test/async-real.test.ts +39 -0
  76. package/test/async.test.ts +375 -0
  77. package/test/base64.test.ts +32 -0
  78. package/test/cache.test.ts +83 -0
  79. package/test/color.test.ts +163 -0
  80. package/test/crypto.test.ts +34 -0
  81. package/test/date-tz.test.ts +206 -0
  82. package/test/date.test.ts +353 -0
  83. package/test/easing.test.ts +33 -0
  84. package/test/emitter.test.ts +71 -0
  85. package/test/enum.test.ts +113 -0
  86. package/test/env.test.ts +69 -0
  87. package/test/error.test.ts +58 -0
  88. package/test/exception.test.ts +43 -0
  89. package/test/fn.test.ts +263 -0
  90. package/test/helpers.ts +23 -0
  91. package/test/index.test.ts +6 -0
  92. package/test/number.test.ts +213 -0
  93. package/test/object.test.ts +309 -0
  94. package/test/path.test.ts +156 -0
  95. package/test/promise.test.ts +199 -0
  96. package/test/qs.test.ts +79 -0
  97. package/test/regexp.test.ts +97 -0
  98. package/test/string.test.ts +150 -0
  99. package/test/time.test.ts +214 -0
  100. package/test/timer.test.ts +114 -0
  101. package/test/tree.test.ts +348 -0
  102. package/test/type.test.ts +226 -0
  103. package/test/unique.test.ts +71 -0
  104. package/test/url.test.ts +136 -0
  105. package/test/version.test.ts +52 -0
  106. package/tsconfig.json +31 -0
  107. package/vite.config.mts +114 -0
package/src/regexp.ts ADDED
@@ -0,0 +1,156 @@
1
+ const specialRE = /[.*+?^=!:${}()|[\]/\\-]/g;
2
+
3
+ /**
4
+ * 编码处理正则表达式
5
+ * @example
6
+ * ```js
7
+ * reEscape('/$')
8
+ * // => '\\/\\$'
9
+ * ```
10
+ * @param {string} string
11
+ * @returns {string}
12
+ */
13
+ export function regexpEscape(string: string): string {
14
+ return string.replace(specialRE, '\\$&');
15
+ }
16
+
17
+ // 邮箱
18
+ const EMAIL_RE = /^\w+[-+.\w]*@([a-z\d-]+\.)+[a-z]{2,5}$/i;
19
+ /**
20
+ * 判断字符串是否为邮箱格式,不对邮箱真实性做验证,如域名是否正确等
21
+ * @param {string} value
22
+ * @returns {boolean}
23
+ */
24
+ export function isEmail(value: string): boolean {
25
+ return EMAIL_RE.test(value);
26
+ }
27
+
28
+ // 手机号码
29
+ const PHONE_RE = /^1\d{10}$/;
30
+ /**
31
+ * 判断字符串是否为宽松手机格式,即首位为 1 的 11 位数字都属于手机号
32
+ * @param {string} value
33
+ * @returns {boolean}
34
+ */
35
+ export function isPhone(value: string): boolean {
36
+ return PHONE_RE.test(value);
37
+ }
38
+
39
+ // 身份证号码
40
+ // http://www.stats.gov.cn/tjsj/tjbz/xzqhdm/
41
+ // ["北京市", "天津市", "河北省", "山西省", "内蒙古自治区",
42
+ // "辽宁省", "吉林省", "黑龙江省",
43
+ // "上海市", "江苏省", "浙江省", "安徽省", "福建省", "江西省", "山东省",
44
+ // "河南省", "湖北省", "湖南省", "广东省", "广西壮族自治区", "海南省",
45
+ // "重庆市", "四川省", "贵州省", "云南省", "西藏自治区",
46
+ // "陕西省", "甘肃省", "青海省","宁夏回族自治区", "新疆维吾尔自治区",
47
+ // "台湾省",
48
+ // "香港特别行政区", "澳门特别行政区"]
49
+ // ["11", "12", "13", "14", "15",
50
+ // "21", "22", "23",
51
+ // "31", "32", "33", "34", "35", "36", "37",
52
+ // "41", "42", "43", "44", "45", "46",
53
+ // "50", "51", "52", "53", "54",
54
+ // "61", "62", "63", "64", "65",
55
+ // "71",
56
+ // "81", "82"]
57
+ // 91 国外
58
+ const IDNO_RE =
59
+ /^(1[1-5]|2[1-3]|3[1-7]|4[1-6]|5[0-4]|6[1-5]|7[1]|8[1-2]|9[1])\d{4}(18|19|20)\d{2}[01]\d[0123]\d{4}[\dxX]$/;
60
+ /**
61
+ * 判断字符串是否为身份证号码格式
62
+ * @param {string} value
63
+ * @returns {boolean}
64
+ */
65
+ export function isIDNo(value: string): boolean {
66
+ const isSameFormat = IDNO_RE.test(value);
67
+
68
+ if (!isSameFormat) return false;
69
+
70
+ const year = Number(value.slice(6, 10));
71
+ const month = Number(value.slice(10, 12));
72
+ const date = Number(value.slice(12, 14));
73
+ const d = new Date(year, month - 1, date);
74
+ const isSameDate = d.getFullYear() === year && d.getMonth() + 1 === month && d.getDate() === date;
75
+
76
+ if (!isSameDate) return false;
77
+
78
+ // 将身份证号码前面的17位数分别乘以不同的系数;
79
+ // 从第一位到第十七位的系数分别为:7-9-10-5-8-4-2-1-6-3-7-9-10-5-8-4-2
80
+ // 将这17位数字和系数相乘的结果相加;
81
+ // 用加出来和除以11,看余数是多少;
82
+ // 余数只可能有0-1-2-3-4-5-6-7-8-9-10这11个数字;
83
+ // 其分别对应的最后一位身份证的号码为1-0-X-9-8-7-6-5-4-3-2
84
+ // 通过上面得知如果余数是2,就会在身份证的第18位数字上出现罗马数字的Ⅹ。如果余数是10,身份证的最后一位号码就是2。
85
+ const coefficientList = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
86
+ const residueList = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
87
+ let sum = 0;
88
+
89
+ for (let start = 0; start < 17; start++) {
90
+ sum += Number(value.slice(start, start + 1)) * coefficientList[start];
91
+ }
92
+
93
+ return residueList[sum % 11] === value.slice(-1);
94
+ }
95
+
96
+ // url
97
+ const URL_RE =
98
+ /^https?:\/\/(([a-z\d-]+\.)+[a-z]{2,5}|(\d{1,3}\.){3}\d{1,3})(:[1-9]\d{0,4})?(\/|\/[\w#!:.?+=&%@'/()-]+)?$/i;
99
+ /**
100
+ * 判断字符串是否为 url 格式,仅支持 http 协议,支持域名或者 ipV4
101
+ * @param {string} value
102
+ * @returns {boolean}
103
+ */
104
+ export function isURL(value: string): boolean {
105
+ return URL_RE.test(value);
106
+ }
107
+
108
+ // ipv4
109
+ const IPV4_RE = /^(?:(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])$/;
110
+ /**
111
+ * 判断字符串是否为 IPV4 格式,不对 ip 真实性做验证
112
+ * @param {string} value
113
+ * @returns {boolean}
114
+ */
115
+ export function isIPV4(value: string): boolean {
116
+ return IPV4_RE.test(value);
117
+ }
118
+
119
+ const INTEGER_RE = /^(-?[1-9]\d*|0)$/;
120
+ /**
121
+ * 判断字符串是否为整数(自然数),即 ...,-3,-2,-1,0,1,2,3,...
122
+ * @param {string} value
123
+ * @returns {boolean}
124
+ */
125
+ export function isInteger(value: string): boolean {
126
+ return INTEGER_RE.test(value);
127
+ }
128
+
129
+ const FLOAT_RE = /^-?([1-9]\d*|0)\.\d+$/;
130
+ /**
131
+ * 判断字符串是否为浮点数,即必须有小数点的有理数
132
+ * @param {string} value
133
+ * @returns {boolean}
134
+ */
135
+ export function isFloat(value: string): boolean {
136
+ return FLOAT_RE.test(value);
137
+ }
138
+
139
+ /**
140
+ * 判断字符串是否为正确数值,包括整数和浮点数
141
+ * @param {string} value
142
+ * @returns {boolean}
143
+ */
144
+ export function isNumerical(value: string): boolean {
145
+ return isInteger(value) || isFloat(value);
146
+ }
147
+
148
+ const DIGIT_RE = /^\d+$/;
149
+ /**
150
+ * 判断字符串是否为数字,例如六位数字短信验证码(093031)
151
+ * @param {string} value
152
+ * @returns {boolean}
153
+ */
154
+ export function isDigit(value: string): boolean {
155
+ return DIGIT_RE.test(value);
156
+ }
package/src/string.ts ADDED
@@ -0,0 +1,146 @@
1
+ import { numberConvert, randomNumber } from './number';
2
+ import { isFunction, isNullish, isNumber, isObject, isString, isUndefined } from './type';
3
+
4
+ export const STRING_ARABIC_NUMERALS = '0123456789';
5
+ export const STRING_HEXADECIMALS = '0123456789abcdef';
6
+ export const STRING_LOWERCASE_ALPHA = 'abcdefghijklmnopqrstuvwxyz';
7
+ export const STRING_UPPERCASE_ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8
+ export const STRING_DICT = `${STRING_ARABIC_NUMERALS + STRING_UPPERCASE_ALPHA + STRING_LOWERCASE_ALPHA}`;
9
+
10
+ /**
11
+ * 将字符串转换为驼峰格式
12
+ * @param {string} string - 要转换的字符串
13
+ * @param {boolean} [bigger] - 是否大写第一个字母,默认为 false
14
+ * @returns {string} - 转换后的驼峰格式字符串
15
+ */
16
+ export function stringCamelCase(string: string, bigger?: boolean): string {
17
+ const string2 = string.replace(/[\s_-](.)/g, (_, char) => (char as string).toUpperCase());
18
+ return bigger ? string2.slice(0, 1).toUpperCase() + string2.slice(1) : string2;
19
+ }
20
+
21
+ /**
22
+ * 将字符串转换为连字格式
23
+ * @param {string} string - 要转换的字符串
24
+ * @param {string} [separator] - 分隔符,默认是 "-"(短横线)
25
+ * @returns {string} - 转换后的连字格式字符串
26
+ */
27
+ export function stringKebabCase(string: string, separator = '-'): string {
28
+ return string.replace(/[A-Z]/g, (origin) => `${separator}${origin.toLowerCase()}`);
29
+ }
30
+
31
+ /**
32
+ * 生成随机字符串
33
+ * @param {number} length - 生成的随机字符串长度
34
+ * @param {string} [dict] - 用于生成随机字符串的字符字典,默认为数字、小写字母和大写字母的组合
35
+ * @returns {string} - 生成的随机字符串
36
+ * @example
37
+ * randomString(10); // 生成一个长度为 10 的随机字符串
38
+ * randomString(8, 'ABCDEF'); // 生成一个长度为 8 的随机字符串,仅包含字符 'ABCDEF'
39
+ */
40
+ export function randomString(length: number, dict?: string): string {
41
+ const dictFinal = dict || STRING_DICT;
42
+ const dictLength = dictFinal.length;
43
+
44
+ let result = '';
45
+
46
+ for (let i = 0; i < length; i++) {
47
+ result += dictFinal.charAt(randomNumber(0, dictLength - 1));
48
+ }
49
+
50
+ return result;
51
+ }
52
+
53
+ /**
54
+ * 简单的模板引擎,类似于 Python 的 `.format()` 方法
55
+ * 支持通过索引或对象/名称的方式传递变量
56
+ * 当使用对象/名称方式时,可以传递一个回退值作为第三个参数
57
+ *
58
+ * @category 字符串
59
+ * @example
60
+ * ```
61
+ * // 索引方式
62
+ * const result = stringFormat(
63
+ * '你好 {0}!我的名字是 {1}。',
64
+ * '张三',
65
+ * '李四'
66
+ * ); // 你好 张三!我的名字是 李四。
67
+ * ```
68
+ *
69
+ * @example
70
+ * ```
71
+ * // 对象方式
72
+ * const result = stringFormat(
73
+ * '{greet}!我的名字是 {name}。',
74
+ * { greet: '你好', name: '王五' }
75
+ * ); // 你好!我的名字是 王五。
76
+ * ```
77
+ *
78
+ * @example
79
+ * ```
80
+ * // 带回退值的对象方式
81
+ * const result = stringFormat(
82
+ * '{greet}!我的名字是 {name}。',
83
+ * { greet: '你好' }, // name 未传递,因此会使用回退值
84
+ * '未知'
85
+ * ); // 你好!我的名字是 未知。
86
+ * ```
87
+ */
88
+ export function stringFormat(
89
+ str: string,
90
+ object: Record<string | number, unknown>,
91
+ fallback?: string | ((key: string) => string),
92
+ ): string;
93
+ export function stringFormat(str: string, ...args: (string | number | bigint | undefined | null)[]): string;
94
+ export function stringFormat(str: string, ...args: unknown[]): string {
95
+ const [firstArg, fallback] = args;
96
+
97
+ if (isObject(firstArg) || isUndefined(firstArg)) {
98
+ const vars = firstArg || {};
99
+ return str.replace(/\{(\w+)\}/g, (_, key) => vars[key] ?? (isFunction(fallback) ? fallback(key) : fallback) ?? key);
100
+ }
101
+
102
+ return str.replace(/\{(\d+)\}/g, (_, key) => {
103
+ const index = Number(key);
104
+ if (Number.isNaN(index)) return key;
105
+ return args[index];
106
+ });
107
+ }
108
+
109
+ /**
110
+ * 生成符合 [RFC 4122](https://www.ietf.org/rfc/rfc4122.txt) 版本 4 的 UUID 字符串
111
+ * @returns {string} - 生成的 UUID 字符串
112
+ * @example
113
+ * const uuid = randomUUID4();
114
+ * console.log(uuid); // 输出类似 '123e4567-e89b-12d3-a456-426614174000' 的 UUID 字符串
115
+ */
116
+ export function randomUUID4(): string {
117
+ const template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
118
+ let result = '';
119
+
120
+ for (let i = 0; i < template.length; i++) {
121
+ const t = template[i];
122
+
123
+ if (t === '-' || t === '4') {
124
+ result += t;
125
+ continue;
126
+ }
127
+
128
+ if (t === 'y') {
129
+ result += randomString(1, '89ab');
130
+ continue;
131
+ }
132
+
133
+ result += randomString(1, STRING_HEXADECIMALS);
134
+ }
135
+
136
+ return result;
137
+ }
138
+
139
+ /**
140
+ * 将值转换为字符串,若值为 null 或 undefined 则返回空字符串
141
+ * @param {unknown} value - 需要转换的值
142
+ * @returns {string} 转换后的字符串结果
143
+ */
144
+ export function stringify(value: unknown) {
145
+ return isNullish(value) ? '' : String(value);
146
+ }
@@ -0,0 +1,57 @@
1
+ // @rer https://day.js.org/docs/en/durations/creating
2
+
3
+ import { DATE_DAY_MS, DATE_HOUR_MS, DATE_MINUTE_MS, DATE_MONTH_MS, DATE_SECOND_MS, DATE_YEAR_MS } from '@/date';
4
+ import { isString } from '@/type';
5
+ import type { TTimeDuration } from './to';
6
+
7
+ /**
8
+ * 时间转换规则数组
9
+ * @type {Array<[RegExp, (match: RegExpMatchArray) => number]>}
10
+ * @property {RegExp} 0 - 匹配时间单位正则表达式
11
+ * @property {function} 1 - 将匹配结果转换为毫秒数的函数
12
+ */
13
+ const rules: [key: keyof TTimeDuration, time: number][] = [
14
+ ['years', DATE_YEAR_MS],
15
+ ['months', DATE_MONTH_MS],
16
+ ['days', DATE_DAY_MS],
17
+ ['hours', DATE_HOUR_MS],
18
+ ['minutes', DATE_MINUTE_MS],
19
+ ['seconds', DATE_SECOND_MS],
20
+ ];
21
+
22
+ /**
23
+ * 将时间持续时间字符串或对象转换为毫秒数
24
+ *
25
+ * @param duration - 可以是时间持续时间字符串(如 '1d2h')或 TTimeDuration 对象
26
+ * @returns 计算得到的总毫秒数
27
+ */
28
+ export function timeFrom(duration: string | TTimeDuration) {
29
+ const td = isString(duration) ? timeParse(duration) : duration;
30
+ return rules.reduce((acc, [key, time]) => acc + (td[key] || 0) * time, 0);
31
+ }
32
+
33
+ const durationMatchRules: [RegExp, key: keyof TTimeDuration][] = [
34
+ [/(\d+)y/i, 'years'],
35
+ [/(\d+)M/, 'months'],
36
+ [/(\d+)d/i, 'days'],
37
+ [/(\d+)h/i, 'hours'],
38
+ [/(\d+)m/, 'minutes'],
39
+ [/(\d+)s/, 'seconds'],
40
+ ];
41
+
42
+ /**
43
+ * 将时长字符串解析为时间对象
44
+ * @param duration - 时长字符串(例如 "1h30m")
45
+ * @returns 包含解析后时间单位的对象(小时、分钟等)
46
+ */
47
+ export function timeParse(duration: string) {
48
+ const result = {} as TTimeDuration;
49
+
50
+ for (const [regex, key] of durationMatchRules) {
51
+ const match = duration.match(regex);
52
+ if (match) result[key] = Number(match[1]);
53
+ else result[key] = 0;
54
+ }
55
+
56
+ return result;
57
+ }
package/src/time/to.ts ADDED
@@ -0,0 +1,106 @@
1
+ import { DATE_DAY_MS, DATE_HOUR_MS, DATE_MINUTE_MS, DATE_SECOND_MS } from '../date';
2
+
3
+ export type TTimeDuration = {
4
+ years: number;
5
+ months: number;
6
+ /** 天数 */
7
+ days: number;
8
+ /** 小时数 */
9
+ hours: number;
10
+ /** 分钟数 */
11
+ minutes: number;
12
+ /** 秒数 */
13
+ seconds: number;
14
+ /** 毫秒数 */
15
+ milliseconds: number;
16
+ };
17
+
18
+ type _TTimeParsePoint = 'D' | 'h' | 'm' | 's' | 'S';
19
+
20
+ /**
21
+ * 解析时间毫秒数为绝对时间对象
22
+ * @param timeMs 时间毫秒数
23
+ * @param maxPoint 最大时间单位(决定分解的起始单位)
24
+ * @returns 包含时间单位分解结果的对象
25
+ * @example
26
+ * ```typescript
27
+ * // 默认以天为最大单位分解
28
+ * timeInDay(123456789);
29
+ * // { days: 1, hours: 10, minutes: 17, seconds: 36, milliseconds: 789 }
30
+ *
31
+ * // 指定最大单位为分钟,分解到分钟及以下单位
32
+ * timeInMinute(123456789);
33
+ * // { days: 0, hours: 0, minutes: 2057, seconds: 36, milliseconds: 789 }
34
+ * ```
35
+ */
36
+ function _timeAbsolute(timeMs: number, maxPoint: _TTimeParsePoint): TTimeDuration {
37
+ const minPoint: _TTimeParsePoint = 'S';
38
+
39
+ const defines: [point: _TTimeParsePoint, key: keyof TTimeDuration, base: number][] = [
40
+ ['D', 'days', DATE_DAY_MS],
41
+ ['h', 'hours', DATE_HOUR_MS],
42
+ ['m', 'minutes', DATE_MINUTE_MS],
43
+ ['s', 'seconds', DATE_SECOND_MS],
44
+ ['S', 'milliseconds', 1],
45
+ ] as const;
46
+
47
+ const minIndex = defines.findIndex((item) => item[0] === maxPoint);
48
+ const maxIndex = defines.findIndex((item) => item[0] === minPoint);
49
+
50
+ let timeMsFinal = timeMs;
51
+ const dao: TTimeDuration = {
52
+ years: 0,
53
+ months: 0,
54
+ days: 0,
55
+ hours: 0,
56
+ minutes: 0,
57
+ seconds: 0,
58
+ milliseconds: 0,
59
+ };
60
+
61
+ for (let i = minIndex; i <= maxIndex; i++) {
62
+ const mode = defines[i];
63
+ const base = mode[2];
64
+ const value = Math.floor(timeMsFinal / base);
65
+ timeMsFinal = timeMsFinal - value * base;
66
+ dao[mode[1]] = value;
67
+ }
68
+
69
+ return dao;
70
+ }
71
+
72
+ /**
73
+ * 将时间毫秒数解析为以天为最大单位的绝对时间对象
74
+ * @param timeMs 时间毫秒数
75
+ * @returns 包含天/小时/分钟/秒/毫秒分解结果的对象
76
+ */
77
+ export function timeToDays(timeMs: number) {
78
+ return _timeAbsolute(timeMs, 'D');
79
+ }
80
+
81
+ /**
82
+ * 将时间毫秒数解析为以小时为最大单位的绝对时间对象
83
+ * @param timeMs 时间毫秒数
84
+ * @returns 包含小时/分钟/秒/毫秒分解结果的对象
85
+ */
86
+ export function timeToHours(timeMs: number) {
87
+ return _timeAbsolute(timeMs, 'h');
88
+ }
89
+
90
+ /**
91
+ * 将时间毫秒数解析为以分钟为最大单位的绝对时间对象
92
+ * @param timeMs 时间毫秒数
93
+ * @returns 包含分钟/秒/毫秒分解结果的对象
94
+ */
95
+ export function timeToMinutes(timeMs: number) {
96
+ return _timeAbsolute(timeMs, 'm');
97
+ }
98
+
99
+ /**
100
+ * 将时间毫秒数解析为以秒为最大单位的绝对时间对象
101
+ * @param timeMs 时间毫秒数
102
+ * @returns 包含秒/毫秒分解结果的对象
103
+ */
104
+ export function timeToSeconds(timeMs: number) {
105
+ return _timeAbsolute(timeMs, 's');
106
+ }
package/src/time.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './time/from';
2
+ export * from './time/to';
package/src/timer.ts ADDED
@@ -0,0 +1,226 @@
1
+ /**
2
+ * 定时器状态接口
3
+ */
4
+ export type TTimerState = {
5
+ /**
6
+ * 执行次数
7
+ */
8
+ times: number;
9
+ /**
10
+ * 开始时间戳
11
+ */
12
+ startAt: number;
13
+ /**
14
+ * 停止时间戳
15
+ */
16
+ stopAt: number;
17
+ /**
18
+ * 暂停时间戳
19
+ */
20
+ pauseAt: number;
21
+ /**
22
+ * 恢复时间戳
23
+ */
24
+ resumeAt: number;
25
+ /**
26
+ * 当前时间戳
27
+ */
28
+ currentAt: number;
29
+ /**
30
+ * 总耗时(包括暂停时间)
31
+ */
32
+ elapsedTime: number;
33
+ /**
34
+ * 实际运行时间(不包括暂停时间)
35
+ */
36
+ runningTime: number;
37
+ /**
38
+ * 当前间隔时间
39
+ */
40
+ intervalTime: number;
41
+ };
42
+
43
+ export type TTimerHandler = {
44
+ /**
45
+ * 开始
46
+ */
47
+ start: () => void;
48
+ /**
49
+ * 暂停
50
+ */
51
+ pause: () => void;
52
+ /**
53
+ * 恢复
54
+ */
55
+ resume: (immediate?: boolean) => void;
56
+ /**
57
+ * 停止
58
+ */
59
+ stop: () => void;
60
+ };
61
+
62
+ const STATUS_READY = 0;
63
+ const STATUS_START = 1;
64
+ const STATUS_PAUSE = 2;
65
+ const STATUS_STOP = 3;
66
+
67
+ /**
68
+ * 创建间隔定时器核心函数
69
+ *
70
+ * @param nextTime - 用于安排下一次执行的函数
71
+ * @param effect - 每次执行的回调函数,接收定时器状态和可选的next函数
72
+ * @returns 返回包含控制方法的对象
73
+ */
74
+ export function makeInterval(
75
+ nextTime: (call: () => void) => void,
76
+ effect: (timer: TTimerState, next?: () => void) => unknown,
77
+ ) {
78
+ let startAt = 0;
79
+ let lastAt = 0;
80
+ let stopAt = 0;
81
+ let pauseAt = 0;
82
+ let resumeAt = 0;
83
+ let times = 0;
84
+ let status = STATUS_READY;
85
+ let runningTime = 0;
86
+
87
+ const execute = () => {
88
+ if (status >= STATUS_PAUSE) return;
89
+
90
+ const now = Date.now();
91
+ const intervalTime = lastAt > 0 ? now - lastAt : 0;
92
+ runningTime += intervalTime;
93
+ lastAt = now;
94
+ const state: TTimerState = {
95
+ times: ++times,
96
+ startAt,
97
+ stopAt,
98
+ pauseAt,
99
+ resumeAt,
100
+ currentAt: now,
101
+ elapsedTime: startAt > 0 ? now - startAt : 0,
102
+ runningTime,
103
+ intervalTime,
104
+ };
105
+
106
+ if (effect.length === 2) {
107
+ effect(state, () => {
108
+ nextTime(execute);
109
+ });
110
+ } else {
111
+ effect(state);
112
+ nextTime(execute);
113
+ }
114
+ };
115
+
116
+ const canStart = () => status === STATUS_READY;
117
+ const start = () => {
118
+ if (!canStart()) return;
119
+ status = STATUS_START;
120
+ startAt = Date.now();
121
+ execute();
122
+ };
123
+
124
+ const canStop = () => status === STATUS_START;
125
+ const stop = () => {
126
+ if (!canStop()) return;
127
+ status = STATUS_STOP;
128
+ stopAt = Date.now();
129
+ };
130
+
131
+ const canPause = () => status === STATUS_START;
132
+ const pause = () => {
133
+ if (!canPause()) return;
134
+ status = STATUS_PAUSE;
135
+ pauseAt = Date.now();
136
+ };
137
+
138
+ const canResume = () => status === STATUS_PAUSE;
139
+ const resume = () => {
140
+ if (!canResume()) return;
141
+ status = STATUS_START;
142
+ resumeAt = Date.now();
143
+ lastAt = resumeAt;
144
+ execute();
145
+ };
146
+
147
+ return {
148
+ canStart,
149
+ canStop,
150
+ canPause,
151
+ canResume,
152
+ start,
153
+ stop,
154
+ pause,
155
+ resume,
156
+ execute,
157
+ };
158
+ }
159
+
160
+ export type TTimerOptions = {
161
+ /**
162
+ * 是否在定时器开始时立即执行回调
163
+ */
164
+ leading?: boolean;
165
+ /**
166
+ * 是否在定时器停止时执行最后一次回调
167
+ */
168
+ trailing?: boolean;
169
+ };
170
+
171
+ /**
172
+ * 创建一个基于 `setTimeout` 的间隔定时器
173
+ *
174
+ * @param callback - 每次间隔执行的回调函数,接收定时器状态和可选的 `next` 函数
175
+ * @param interval - 间隔时间,单位为毫秒
176
+ * @param options - 配置选项
177
+ * @returns {TTimerHandler}
178
+ */
179
+ export function timeInterval(
180
+ callback: (state: TTimerState, next?: () => void) => unknown,
181
+ interval: number,
182
+ options?: TTimerOptions,
183
+ ): TTimerHandler {
184
+ let timeId: number | NodeJS.Timeout;
185
+ const { canStart, canStop, canPause, canResume, start, stop, pause, resume, execute } = makeInterval((call) => {
186
+ timeId = setTimeout(call, interval);
187
+ }, callback);
188
+
189
+ return {
190
+ start() {
191
+ if (!canStart()) return;
192
+
193
+ if (options?.leading) {
194
+ start();
195
+ } else {
196
+ timeId = setTimeout(start, interval);
197
+ }
198
+ },
199
+
200
+ stop() {
201
+ if (!canStop()) return;
202
+ if (options?.trailing) execute();
203
+
204
+ clearTimeout(timeId);
205
+ stop();
206
+ },
207
+
208
+ pause() {
209
+ if (!canPause()) return;
210
+ if (options?.trailing) execute();
211
+
212
+ clearTimeout(timeId);
213
+ pause();
214
+ },
215
+
216
+ resume(immediate?: boolean) {
217
+ if (!canResume()) return;
218
+
219
+ if (immediate || options?.leading) {
220
+ resume();
221
+ } else {
222
+ timeId = setTimeout(resume, interval);
223
+ }
224
+ },
225
+ };
226
+ }