ansuko 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.
- package/README.ja.md +343 -0
- package/README.md +353 -0
- package/dist/index.d.ts +196 -0
- package/dist/index.js +506 -0
- package/dist/plugins/geo.d.ts +25 -0
- package/dist/plugins/geo.js +566 -0
- package/dist/plugins/ja.d.ts +13 -0
- package/dist/plugins/ja.js +149 -0
- package/dist/plugins/prototype.d.ts +22 -0
- package/dist/plugins/prototype.js +9 -0
- package/dist/util.d.ts +23 -0
- package/dist/util.js +104 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +8 -0
- package/package.json +84 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import _ from "lodash";
|
|
2
|
+
/**
|
|
3
|
+
* Checks if the value is a non-empty string. null/undefined/empty string -> false.
|
|
4
|
+
* @param str - Value to check
|
|
5
|
+
* @returns true if non-empty string
|
|
6
|
+
* @example isValidStr('hello') // true
|
|
7
|
+
* @example isValidStr('') // false
|
|
8
|
+
* @category Type Guards
|
|
9
|
+
*/
|
|
10
|
+
declare const isValidStr: (str: unknown) => str is string;
|
|
11
|
+
/**
|
|
12
|
+
* Returns value or a default. Detects functions and Promises automatically.
|
|
13
|
+
* @param value - Value or thunk
|
|
14
|
+
* @param els - Default value or thunk
|
|
15
|
+
* @returns Value or default
|
|
16
|
+
* @example valueOr('v','d') // 'v'
|
|
17
|
+
* @example valueOr(asyncFetch(), 'fallback') // Promise resolves to fetched or fallback
|
|
18
|
+
* @example await valueOr(() => cache.get(id), () => api.fetch(id))
|
|
19
|
+
* @category Promise Utilities
|
|
20
|
+
*/
|
|
21
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
22
|
+
type MaybeFunction<T> = T | (() => MaybePromise<T>);
|
|
23
|
+
declare const valueOr: <T, E>(value: MaybeFunction<MaybePromise<T | null | undefined>>, els?: E | (() => MaybePromise<E>)) => MaybePromise<T | E | undefined | null>;
|
|
24
|
+
declare const emptyOr: <T, E>(value: MaybeFunction<MaybePromise<T | null | undefined>>, els?: E | ((val: T | null | undefined) => MaybePromise<E>)) => MaybePromise<T | E | undefined | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Checks emptiness with intuitive rules: numbers and booleans are NOT empty.
|
|
27
|
+
* @param value - Value to check
|
|
28
|
+
* @returns true if empty
|
|
29
|
+
* @example isEmpty(0) // false
|
|
30
|
+
* @example isEmpty([]) // true
|
|
31
|
+
* @category Core Functions
|
|
32
|
+
*/
|
|
33
|
+
declare const isEmpty: (value: unknown) => boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Converts a value to number (full-width and comma aware). Returns null when invalid.
|
|
36
|
+
* @param value - Value to convert
|
|
37
|
+
* @returns number or null
|
|
38
|
+
* @example toNumber('1,234.5') // 1234.5
|
|
39
|
+
* @example toNumber('123') // 123
|
|
40
|
+
* @example toNumber('abc') // null
|
|
41
|
+
* @category Core Functions
|
|
42
|
+
*/
|
|
43
|
+
declare const toNumber: (value: unknown) => number | null;
|
|
44
|
+
/**
|
|
45
|
+
* Converts various inputs to boolean. Numbers: 0 -> false, non-zero -> true.
|
|
46
|
+
* Strings: 'true'|'t'|'y'|'yes'|'ok' -> true; 'false'|'f'|'n'|'no'|'ng' -> false.
|
|
47
|
+
* If a function or Promise is provided, it will be resolved recursively.
|
|
48
|
+
* Returns the `undetected` fallback when the value cannot be interpreted (default null).
|
|
49
|
+
* @param value - Value, thunk, or Promise
|
|
50
|
+
* @param undetected - Fallback when value cannot be interpreted (default null)
|
|
51
|
+
* @returns boolean or null (sync or Promise)
|
|
52
|
+
* @category Core Functions
|
|
53
|
+
*/
|
|
54
|
+
declare const toBool: (value: unknown, undetected?: boolean | null) => MaybePromise<boolean | null>;
|
|
55
|
+
/**
|
|
56
|
+
* Safely converts to boolean; numbers use zero check; otherwise returns the provided default.
|
|
57
|
+
* @param value - Value
|
|
58
|
+
* @param defaultValue - Default when value is not number/boolean (false)
|
|
59
|
+
* @returns boolean
|
|
60
|
+
* @example boolIf(1) // true
|
|
61
|
+
* @example boolIf('x', true) // true
|
|
62
|
+
* @category Core Functions
|
|
63
|
+
*/
|
|
64
|
+
declare const boolIf: (value: unknown, defaultValue?: boolean) => boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Runs a function after N frames using requestAnimationFrame.
|
|
67
|
+
* @param func - Function to run
|
|
68
|
+
* @param frameCount - Frames to wait (default 0)
|
|
69
|
+
* @example waited(() => doMeasure(), 1)
|
|
70
|
+
* @example waited(startAnimation, 2)
|
|
71
|
+
* @category Core Functions
|
|
72
|
+
*/
|
|
73
|
+
declare const waited: (func: () => void, frameCount?: number) => void;
|
|
74
|
+
/**
|
|
75
|
+
* Compares two values; if equal returns the value, otherwise returns the default.
|
|
76
|
+
* null and undefined are considered equal. Promise-aware.
|
|
77
|
+
* @param param1 - First value (or thunk/promise)
|
|
78
|
+
* @param param2 - Second value (or thunk/promise)
|
|
79
|
+
* @param els - Default value (or thunk)
|
|
80
|
+
* @returns Value or default (sync or Promise)
|
|
81
|
+
* @example equalsOr('a','a','d') // 'a'
|
|
82
|
+
* @example await equalsOr(fetchStatus(),'ok','ng')
|
|
83
|
+
* @example equalsOr(null, undefined, 'd') // null
|
|
84
|
+
* @category Promise Utilities
|
|
85
|
+
*/
|
|
86
|
+
declare const equalsOr: <T, E>(...args: any[]) => MaybePromise<T | E | null>;
|
|
87
|
+
/**
|
|
88
|
+
* Safely parses JSON/JSON5; returns null on error; passes objects through.
|
|
89
|
+
* @param str - String or object
|
|
90
|
+
* @returns Parsed object or null
|
|
91
|
+
* @example parseJSON('{"a":1}') // {a:1}
|
|
92
|
+
* @example parseJSON('{a:1}') // {a:1} (JSON5)
|
|
93
|
+
* @category Conversion
|
|
94
|
+
*/
|
|
95
|
+
declare const parseJSON: <T = any>(str: string | object) => T | null;
|
|
96
|
+
/**
|
|
97
|
+
* Stringifies objects/arrays; returns null for strings or numbers.
|
|
98
|
+
* @param obj - Target object
|
|
99
|
+
* @returns JSON string or null
|
|
100
|
+
* @example jsonStringify({a:1}) // '{"a":1}'
|
|
101
|
+
* @example jsonStringify('{a:1}') // '{"a":1}' (normalize)
|
|
102
|
+
* @category Conversion
|
|
103
|
+
*/
|
|
104
|
+
declare const jsonStringify: <T = any>(obj: T) => string | null;
|
|
105
|
+
/**
|
|
106
|
+
* Casts value to array; null/undefined become [] (not [null]).
|
|
107
|
+
* @param value - Value
|
|
108
|
+
* @returns Array
|
|
109
|
+
* @example castArray(1) // [1]
|
|
110
|
+
* @example castArray(null) // []
|
|
111
|
+
* @category Array Utilities
|
|
112
|
+
*/
|
|
113
|
+
declare const castArray: <T>(value: T | T[] | null | undefined) => T[];
|
|
114
|
+
/**
|
|
115
|
+
* Computes differences between two objects. Supports deep paths. When `keyExcludes` is true,
|
|
116
|
+
* the provided keys are excluded (top-level only) from diffing.
|
|
117
|
+
* @param sourceValue - Source object
|
|
118
|
+
* @param currentValue - Current object
|
|
119
|
+
* @param keys - Keys to check (deep paths allowed)
|
|
120
|
+
* @param options - { keyExcludes } to treat keys as excludes (top-level only)
|
|
121
|
+
* @param finallyCallback - Called after diff computation (always if notEmptyCallback returned truthy)
|
|
122
|
+
* @param notEmptyCallback - Called only when diff is not empty
|
|
123
|
+
* @returns Diff object
|
|
124
|
+
* @example changes(o1,o2,['name','profile.bio'])
|
|
125
|
+
* @example changes(orig, curr, ['id','created_at'], { keyExcludes:true })
|
|
126
|
+
* @example await changes(a,b,['x'],{}, async diff => save(diff))
|
|
127
|
+
* @example changes(
|
|
128
|
+
* { profile:{ tags:['a','b'] } },
|
|
129
|
+
* { profile:{ tags:['a','c'] } },
|
|
130
|
+
* ['profile.tags[1]']
|
|
131
|
+
* ) // => { profile:{ tags:{ 1:'c' } } }
|
|
132
|
+
* @category Object Utilities
|
|
133
|
+
*/
|
|
134
|
+
declare const changes: <T extends Record<string, any>, E extends Record<string, any>>(sourceValue: T, currentValue: E, keys: string[], options?: ChangesOptions, finallyCallback?: ChangesAfterFinallyCallback<Record<string, any>>, notEmptyCallback?: ChangesAfterCallback<Record<string, any>>) => Record<string, any>;
|
|
135
|
+
/**
|
|
136
|
+
* Returns nesting depth of arrays. Non-array: 0; empty array: 1. Uses minimum depth for mixed nesting.
|
|
137
|
+
* @param ary - Array
|
|
138
|
+
* @returns Depth
|
|
139
|
+
* @example arrayDepth([1]) // 1
|
|
140
|
+
* @example arrayDepth([[1],[2]]) // 2
|
|
141
|
+
* @example arrayDepth([[[1]]]) // 3
|
|
142
|
+
* @example arrayDepth([[1,2], [[3],[4,5]]]) // 2 (uses minimum depth)
|
|
143
|
+
* @example arrayDepth([]) // 1
|
|
144
|
+
* @example arrayDepth('not array') // 0
|
|
145
|
+
* @category Array Utilities
|
|
146
|
+
*/
|
|
147
|
+
declare const arrayDepth: (ary: unknown) => number;
|
|
148
|
+
/**
|
|
149
|
+
* Extends ansuko with a plugin and returns the augmented instance.
|
|
150
|
+
* @param plugin - Plugin function
|
|
151
|
+
* @returns Extended instance
|
|
152
|
+
* @example const extended = _.extend(jaPlugin)
|
|
153
|
+
* @category Core Functions
|
|
154
|
+
*/
|
|
155
|
+
declare const extend: <T>(this: any, plugin: (a: any) => T) => any & T;
|
|
156
|
+
export type ChangesOptions = {
|
|
157
|
+
keyExcludes?: boolean;
|
|
158
|
+
};
|
|
159
|
+
type ChangesAfterCallback<T> = (value: T) => any | Promise<any>;
|
|
160
|
+
type ChangesAfterFinallyCallback<T> = (value: T, res: any) => any | Promise<any>;
|
|
161
|
+
export interface AnsukoType extends Omit<_.LoDashStatic, "castArray" | "isEmpty" | "toNumber"> {
|
|
162
|
+
extend: typeof extend;
|
|
163
|
+
isValidStr: typeof isValidStr;
|
|
164
|
+
valueOr: typeof valueOr;
|
|
165
|
+
emptyOr: typeof emptyOr;
|
|
166
|
+
isEmpty: typeof isEmpty;
|
|
167
|
+
toNumber: typeof toNumber;
|
|
168
|
+
toBool: typeof toBool;
|
|
169
|
+
boolIf: typeof boolIf;
|
|
170
|
+
waited: typeof waited;
|
|
171
|
+
equalsOr: typeof equalsOr;
|
|
172
|
+
parseJSON: typeof parseJSON;
|
|
173
|
+
jsonStringify: typeof jsonStringify;
|
|
174
|
+
castArray: typeof castArray;
|
|
175
|
+
changes: typeof changes;
|
|
176
|
+
size: typeof _.size;
|
|
177
|
+
isNil: typeof _.isNil;
|
|
178
|
+
debounce: typeof _.debounce;
|
|
179
|
+
isEqual: typeof _.isEqual;
|
|
180
|
+
isBoolean: typeof _.isBoolean;
|
|
181
|
+
isString: typeof _.isString;
|
|
182
|
+
first: typeof _.first;
|
|
183
|
+
last: typeof _.last;
|
|
184
|
+
uniq: typeof _.uniq;
|
|
185
|
+
has: typeof _.has;
|
|
186
|
+
keys: typeof _.keys;
|
|
187
|
+
values: typeof _.values;
|
|
188
|
+
some: typeof _.some;
|
|
189
|
+
arrayDepth: typeof arrayDepth;
|
|
190
|
+
isEmptyOrg: typeof _.isEmpty;
|
|
191
|
+
toNumberOrg: typeof _.toNumber;
|
|
192
|
+
castArrayOrg: typeof _.castArray;
|
|
193
|
+
}
|
|
194
|
+
declare const _default: AnsukoType;
|
|
195
|
+
export default _default;
|
|
196
|
+
export { isEmpty, toNumber, boolIf, isValidStr, valueOr, equalsOr, waited, parseJSON, jsonStringify, castArray, changes, arrayDepth, };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import _ from "lodash";
|
|
2
|
+
import JSON5 from "json5";
|
|
3
|
+
import { toHalfWidth } from "./util.js";
|
|
4
|
+
/**
|
|
5
|
+
* Checks if the value is a non-empty string. null/undefined/empty string -> false.
|
|
6
|
+
* @param str - Value to check
|
|
7
|
+
* @returns true if non-empty string
|
|
8
|
+
* @example isValidStr('hello') // true
|
|
9
|
+
* @example isValidStr('') // false
|
|
10
|
+
* @category Type Guards
|
|
11
|
+
*/
|
|
12
|
+
const isValidStr = (str) => {
|
|
13
|
+
if (_.isNil(str)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (_.isEmpty(str)) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return typeof str === "string";
|
|
20
|
+
};
|
|
21
|
+
const valueOr = (value, els) => {
|
|
22
|
+
// 関数を解決
|
|
23
|
+
const resolvedValue = typeof value === "function"
|
|
24
|
+
? value()
|
|
25
|
+
: value;
|
|
26
|
+
// Promiseかチェック
|
|
27
|
+
if (resolvedValue instanceof Promise) {
|
|
28
|
+
return Promise.resolve(resolvedValue).then(res => {
|
|
29
|
+
if (_.isNil(res) || isEmpty(res)) {
|
|
30
|
+
if (typeof els === "function") {
|
|
31
|
+
return els();
|
|
32
|
+
}
|
|
33
|
+
return els;
|
|
34
|
+
}
|
|
35
|
+
return res;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (!_.isNil(resolvedValue) && !isEmpty(resolvedValue)) {
|
|
39
|
+
return resolvedValue;
|
|
40
|
+
}
|
|
41
|
+
if (typeof els === "function") {
|
|
42
|
+
return els();
|
|
43
|
+
}
|
|
44
|
+
return els;
|
|
45
|
+
};
|
|
46
|
+
const emptyOr = (value, els) => {
|
|
47
|
+
// 関数を解決
|
|
48
|
+
const resolvedValue = typeof value === "function"
|
|
49
|
+
? value()
|
|
50
|
+
: value;
|
|
51
|
+
// Promiseかチェック
|
|
52
|
+
if (resolvedValue instanceof Promise) {
|
|
53
|
+
return Promise.resolve(resolvedValue).then(res => {
|
|
54
|
+
if (_.isNil(res) || isEmpty(res)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
if (typeof els === "function") {
|
|
58
|
+
return els(res);
|
|
59
|
+
}
|
|
60
|
+
return els;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (_.isNil(resolvedValue) || isEmpty(resolvedValue)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (typeof els === "function") {
|
|
67
|
+
return els(resolvedValue);
|
|
68
|
+
}
|
|
69
|
+
return els;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Ensures that all given paths exist on the resolved value; otherwise returns a default.
|
|
73
|
+
* Supports functions and Promises.
|
|
74
|
+
* @param value - Object or thunk
|
|
75
|
+
* @param paths - Paths to check
|
|
76
|
+
* @param els - Default value or thunk receiving the resolved value
|
|
77
|
+
* @returns Value or default
|
|
78
|
+
* @example await hasOr(fetchUser(), ['profile.name','id'], null)
|
|
79
|
+
* @example hasOr({a:{b:1}}, 'a.b', {}) // returns original object
|
|
80
|
+
* @category Promise Utilities
|
|
81
|
+
*/
|
|
82
|
+
const hasOr = (value, paths, els) => {
|
|
83
|
+
// 関数を解決
|
|
84
|
+
const resolvedValue = typeof value === "function"
|
|
85
|
+
? value()
|
|
86
|
+
: value;
|
|
87
|
+
const pathArray = Array.isArray(paths) ? paths : [paths];
|
|
88
|
+
// パスが全て存在するかチェック
|
|
89
|
+
const checkPaths = (val) => {
|
|
90
|
+
if (_.isNil(val) || isEmpty(val))
|
|
91
|
+
return false;
|
|
92
|
+
return pathArray.every(path => _.has(val, path));
|
|
93
|
+
};
|
|
94
|
+
// Promiseかチェック
|
|
95
|
+
if (resolvedValue instanceof Promise) {
|
|
96
|
+
return Promise.resolve(resolvedValue).then(res => {
|
|
97
|
+
if (!checkPaths(res)) {
|
|
98
|
+
if (typeof els === "function") {
|
|
99
|
+
return els(res);
|
|
100
|
+
}
|
|
101
|
+
return els;
|
|
102
|
+
}
|
|
103
|
+
return res;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (!checkPaths(resolvedValue)) {
|
|
107
|
+
if (typeof els === "function") {
|
|
108
|
+
return els(resolvedValue);
|
|
109
|
+
}
|
|
110
|
+
return els;
|
|
111
|
+
}
|
|
112
|
+
return resolvedValue;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Checks emptiness with intuitive rules: numbers and booleans are NOT empty.
|
|
116
|
+
* @param value - Value to check
|
|
117
|
+
* @returns true if empty
|
|
118
|
+
* @example isEmpty(0) // false
|
|
119
|
+
* @example isEmpty([]) // true
|
|
120
|
+
* @category Core Functions
|
|
121
|
+
*/
|
|
122
|
+
const isEmpty = (value) => {
|
|
123
|
+
if (_.isNil(value)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (_.isNumber(value)) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
if (_.isBoolean(value)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return _.isEmpty(value);
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Converts a value to number (full-width and comma aware). Returns null when invalid.
|
|
136
|
+
* @param value - Value to convert
|
|
137
|
+
* @returns number or null
|
|
138
|
+
* @example toNumber('1,234.5') // 1234.5
|
|
139
|
+
* @example toNumber('123') // 123
|
|
140
|
+
* @example toNumber('abc') // null
|
|
141
|
+
* @category Core Functions
|
|
142
|
+
*/
|
|
143
|
+
const toNumber = (value) => {
|
|
144
|
+
if (_.isNil(value)) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (_.isNumber(value)) {
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
if (isEmpty(value)) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
let v = toHalfWidth(value);
|
|
154
|
+
if (typeof v === "string" && v.trim().match(/^[0-9][0-9,.]*$/)) {
|
|
155
|
+
v = _.toNumber(v.trim().replace(/,/g, ""));
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
v = _.toNumber(v);
|
|
159
|
+
}
|
|
160
|
+
return _.isNaN(v) ? null : v;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Converts various inputs to boolean. Numbers: 0 -> false, non-zero -> true.
|
|
164
|
+
* Strings: 'true'|'t'|'y'|'yes'|'ok' -> true; 'false'|'f'|'n'|'no'|'ng' -> false.
|
|
165
|
+
* If a function or Promise is provided, it will be resolved recursively.
|
|
166
|
+
* Returns the `undetected` fallback when the value cannot be interpreted (default null).
|
|
167
|
+
* @param value - Value, thunk, or Promise
|
|
168
|
+
* @param undetected - Fallback when value cannot be interpreted (default null)
|
|
169
|
+
* @returns boolean or null (sync or Promise)
|
|
170
|
+
* @category Core Functions
|
|
171
|
+
*/
|
|
172
|
+
const toBool = (value, undetected = null) => {
|
|
173
|
+
if (_.isNil(value)) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
if (isEmpty(value)) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
if (_.isBoolean(value)) {
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
if (_.isNumber(value)) {
|
|
183
|
+
return value !== 0;
|
|
184
|
+
}
|
|
185
|
+
const n = toNumber(value);
|
|
186
|
+
if (n !== null)
|
|
187
|
+
return !!n;
|
|
188
|
+
if (typeof value === "string") {
|
|
189
|
+
switch (value.toLowerCase()) {
|
|
190
|
+
case "true":
|
|
191
|
+
case "t":
|
|
192
|
+
case "y":
|
|
193
|
+
case "yes":
|
|
194
|
+
case "ok":
|
|
195
|
+
return true;
|
|
196
|
+
case "false":
|
|
197
|
+
case "f":
|
|
198
|
+
case "n":
|
|
199
|
+
case "no":
|
|
200
|
+
case "ng":
|
|
201
|
+
return false;
|
|
202
|
+
default:
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (typeof value === "function") {
|
|
206
|
+
const r = value();
|
|
207
|
+
if (r instanceof Promise) {
|
|
208
|
+
return r.then(toBool);
|
|
209
|
+
}
|
|
210
|
+
return toBool(r);
|
|
211
|
+
}
|
|
212
|
+
return undetected;
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Safely converts to boolean; numbers use zero check; otherwise returns the provided default.
|
|
216
|
+
* @param value - Value
|
|
217
|
+
* @param defaultValue - Default when value is not number/boolean (false)
|
|
218
|
+
* @returns boolean
|
|
219
|
+
* @example boolIf(1) // true
|
|
220
|
+
* @example boolIf('x', true) // true
|
|
221
|
+
* @category Core Functions
|
|
222
|
+
*/
|
|
223
|
+
const boolIf = (value, defaultValue = false) => {
|
|
224
|
+
if (_.isBoolean(value)) {
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
if (_.isNumber(value)) {
|
|
228
|
+
return !!value;
|
|
229
|
+
}
|
|
230
|
+
return defaultValue;
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* Runs a function after N frames using requestAnimationFrame.
|
|
234
|
+
* @param func - Function to run
|
|
235
|
+
* @param frameCount - Frames to wait (default 0)
|
|
236
|
+
* @example waited(() => doMeasure(), 1)
|
|
237
|
+
* @example waited(startAnimation, 2)
|
|
238
|
+
* @category Core Functions
|
|
239
|
+
*/
|
|
240
|
+
const waited = (func, frameCount = 0) => {
|
|
241
|
+
requestAnimationFrame(() => {
|
|
242
|
+
if (frameCount > 0) {
|
|
243
|
+
return waited(func, frameCount - 1);
|
|
244
|
+
}
|
|
245
|
+
func();
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
/**
|
|
249
|
+
* Compares two values; if equal returns the value, otherwise returns the default.
|
|
250
|
+
* null and undefined are considered equal. Promise-aware.
|
|
251
|
+
* @param param1 - First value (or thunk/promise)
|
|
252
|
+
* @param param2 - Second value (or thunk/promise)
|
|
253
|
+
* @param els - Default value (or thunk)
|
|
254
|
+
* @returns Value or default (sync or Promise)
|
|
255
|
+
* @example equalsOr('a','a','d') // 'a'
|
|
256
|
+
* @example await equalsOr(fetchStatus(),'ok','ng')
|
|
257
|
+
* @example equalsOr(null, undefined, 'd') // null
|
|
258
|
+
* @category Promise Utilities
|
|
259
|
+
*/
|
|
260
|
+
const equalsOr = (...args) => {
|
|
261
|
+
if (args.length === 2) {
|
|
262
|
+
return valueOr(args[0], args[1]);
|
|
263
|
+
}
|
|
264
|
+
const [param1, param2, els] = args;
|
|
265
|
+
// 関数を解決するヘルパー
|
|
266
|
+
const resolveIfFunction = (val) => {
|
|
267
|
+
return typeof val === "function" ? val() : val;
|
|
268
|
+
};
|
|
269
|
+
// Promiseが含まれているかチェック
|
|
270
|
+
const p1 = resolveIfFunction(param1);
|
|
271
|
+
const p2 = resolveIfFunction(param2);
|
|
272
|
+
const hasPromise = (p1 instanceof Promise) || (p2 instanceof Promise) || (els instanceof Promise);
|
|
273
|
+
if (hasPromise) {
|
|
274
|
+
// Promise処理ブランチ
|
|
275
|
+
return Promise.all([
|
|
276
|
+
Promise.resolve(p1),
|
|
277
|
+
Promise.resolve(p2)
|
|
278
|
+
]).then(([v1, v2]) => {
|
|
279
|
+
if (_.isNil(v1) && _.isNil(v2)) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
if (_.isEqual(v1, v2)) {
|
|
283
|
+
return v1;
|
|
284
|
+
}
|
|
285
|
+
// elsの解決
|
|
286
|
+
if (typeof els === "function") {
|
|
287
|
+
return els();
|
|
288
|
+
}
|
|
289
|
+
return els;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
// 同期処理ブランチ
|
|
294
|
+
if (_.isNil(p1) && _.isNil(p2)) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
if (_.isEqual(p1, p2)) {
|
|
298
|
+
return p1;
|
|
299
|
+
}
|
|
300
|
+
// elsの解決
|
|
301
|
+
if (typeof els === "function") {
|
|
302
|
+
return els();
|
|
303
|
+
}
|
|
304
|
+
return els;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
/**
|
|
308
|
+
* Safely parses JSON/JSON5; returns null on error; passes objects through.
|
|
309
|
+
* @param str - String or object
|
|
310
|
+
* @returns Parsed object or null
|
|
311
|
+
* @example parseJSON('{"a":1}') // {a:1}
|
|
312
|
+
* @example parseJSON('{a:1}') // {a:1} (JSON5)
|
|
313
|
+
* @category Conversion
|
|
314
|
+
*/
|
|
315
|
+
const parseJSON = (str) => {
|
|
316
|
+
if (_.isNil(str)) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
if (typeof str === "object") {
|
|
320
|
+
return str;
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
return JSON5.parse(str);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
/**
|
|
330
|
+
* Stringifies objects/arrays; returns null for strings or numbers.
|
|
331
|
+
* @param obj - Target object
|
|
332
|
+
* @returns JSON string or null
|
|
333
|
+
* @example jsonStringify({a:1}) // '{"a":1}'
|
|
334
|
+
* @example jsonStringify('{a:1}') // '{"a":1}' (normalize)
|
|
335
|
+
* @category Conversion
|
|
336
|
+
*/
|
|
337
|
+
const jsonStringify = (obj) => {
|
|
338
|
+
if (_.isNil(obj)) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
if (typeof obj === "string") {
|
|
342
|
+
try {
|
|
343
|
+
const j = JSON5.parse(obj);
|
|
344
|
+
return JSON.stringify(j);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (typeof obj === "object") {
|
|
351
|
+
try {
|
|
352
|
+
return JSON.stringify(obj);
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
};
|
|
360
|
+
/**
|
|
361
|
+
* Casts value to array; null/undefined become [] (not [null]).
|
|
362
|
+
* @param value - Value
|
|
363
|
+
* @returns Array
|
|
364
|
+
* @example castArray(1) // [1]
|
|
365
|
+
* @example castArray(null) // []
|
|
366
|
+
* @category Array Utilities
|
|
367
|
+
*/
|
|
368
|
+
const castArray = (value) => {
|
|
369
|
+
if (_.isNil(value)) {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
return _.castArray(value);
|
|
373
|
+
};
|
|
374
|
+
/**
|
|
375
|
+
* Computes differences between two objects. Supports deep paths. When `keyExcludes` is true,
|
|
376
|
+
* the provided keys are excluded (top-level only) from diffing.
|
|
377
|
+
* @param sourceValue - Source object
|
|
378
|
+
* @param currentValue - Current object
|
|
379
|
+
* @param keys - Keys to check (deep paths allowed)
|
|
380
|
+
* @param options - { keyExcludes } to treat keys as excludes (top-level only)
|
|
381
|
+
* @param finallyCallback - Called after diff computation (always if notEmptyCallback returned truthy)
|
|
382
|
+
* @param notEmptyCallback - Called only when diff is not empty
|
|
383
|
+
* @returns Diff object
|
|
384
|
+
* @example changes(o1,o2,['name','profile.bio'])
|
|
385
|
+
* @example changes(orig, curr, ['id','created_at'], { keyExcludes:true })
|
|
386
|
+
* @example await changes(a,b,['x'],{}, async diff => save(diff))
|
|
387
|
+
* @example changes(
|
|
388
|
+
* { profile:{ tags:['a','b'] } },
|
|
389
|
+
* { profile:{ tags:['a','c'] } },
|
|
390
|
+
* ['profile.tags[1]']
|
|
391
|
+
* ) // => { profile:{ tags:{ 1:'c' } } }
|
|
392
|
+
* @category Object Utilities
|
|
393
|
+
*/
|
|
394
|
+
const changes = (sourceValue, currentValue, keys, options, finallyCallback, notEmptyCallback) => {
|
|
395
|
+
const diff = {};
|
|
396
|
+
// keyExcludes時にdeep pathが指定されていたら警告
|
|
397
|
+
if (options?.keyExcludes === true) {
|
|
398
|
+
const hasDeepPath = keys.some(k => k.includes('.') || k.includes('['));
|
|
399
|
+
if (hasDeepPath) {
|
|
400
|
+
console.warn('[ansuko.changes] keyExcludes mode does not support deep paths. ' +
|
|
401
|
+
'Keys with "." or "[" will be treated as literal property names.');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const targetKeys = options?.keyExcludes === true
|
|
405
|
+
? _.difference(_.uniq([...Object.keys(sourceValue), ...Object.keys(currentValue)]), keys)
|
|
406
|
+
: keys;
|
|
407
|
+
for (const key of targetKeys) {
|
|
408
|
+
const v1 = options?.keyExcludes === true ? sourceValue[key] : _.get(sourceValue, key);
|
|
409
|
+
const v2 = options?.keyExcludes === true ? currentValue[key] : _.get(currentValue, key);
|
|
410
|
+
if (_.isNil(v1) && _.isNil(v2))
|
|
411
|
+
continue;
|
|
412
|
+
if (_.isNil(v1) || _.isNil(v2)) {
|
|
413
|
+
if (options?.keyExcludes === true) {
|
|
414
|
+
diff[key] = v2 ?? null;
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
_.set(diff, key, v2 ?? null);
|
|
418
|
+
}
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (!_.isEqual(v1, v2)) {
|
|
422
|
+
if (options?.keyExcludes === true) {
|
|
423
|
+
diff[key] = v2 ?? null;
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
_.set(diff, key, v2 ?? null);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
let notEmptyRes = true;
|
|
431
|
+
if (!isEmpty(diff) && notEmptyCallback) {
|
|
432
|
+
notEmptyRes = notEmptyCallback(diff);
|
|
433
|
+
if (notEmptyRes instanceof Promise) {
|
|
434
|
+
return notEmptyRes.then(async (res) => {
|
|
435
|
+
res && await Promise.resolve(finallyCallback?.(diff, res));
|
|
436
|
+
return diff;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (finallyCallback && notEmptyRes) {
|
|
441
|
+
const finallyRes = finallyCallback(diff, notEmptyRes);
|
|
442
|
+
if (finallyRes instanceof Promise) {
|
|
443
|
+
return finallyRes.then(() => diff);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return diff;
|
|
447
|
+
};
|
|
448
|
+
/**
|
|
449
|
+
* Returns nesting depth of arrays. Non-array: 0; empty array: 1. Uses minimum depth for mixed nesting.
|
|
450
|
+
* @param ary - Array
|
|
451
|
+
* @returns Depth
|
|
452
|
+
* @example arrayDepth([1]) // 1
|
|
453
|
+
* @example arrayDepth([[1],[2]]) // 2
|
|
454
|
+
* @example arrayDepth([[[1]]]) // 3
|
|
455
|
+
* @example arrayDepth([[1,2], [[3],[4,5]]]) // 2 (uses minimum depth)
|
|
456
|
+
* @example arrayDepth([]) // 1
|
|
457
|
+
* @example arrayDepth('not array') // 0
|
|
458
|
+
* @category Array Utilities
|
|
459
|
+
*/
|
|
460
|
+
const arrayDepth = (ary) => {
|
|
461
|
+
if (!Array.isArray(ary)) {
|
|
462
|
+
return 0;
|
|
463
|
+
}
|
|
464
|
+
if (_.size(ary) === 0) {
|
|
465
|
+
return 1;
|
|
466
|
+
}
|
|
467
|
+
return 1 + Math.min(...ary.map(arrayDepth));
|
|
468
|
+
};
|
|
469
|
+
/**
|
|
470
|
+
* Extends ansuko with a plugin and returns the augmented instance.
|
|
471
|
+
* @param plugin - Plugin function
|
|
472
|
+
* @returns Extended instance
|
|
473
|
+
* @example const extended = _.extend(jaPlugin)
|
|
474
|
+
* @category Core Functions
|
|
475
|
+
*/
|
|
476
|
+
const extend = function (plugin) {
|
|
477
|
+
if (typeof plugin === 'function') {
|
|
478
|
+
plugin(this); // ← this が undefined になってる?
|
|
479
|
+
}
|
|
480
|
+
return this;
|
|
481
|
+
};
|
|
482
|
+
// Ansuko型へのキャストを外し、より安全な unknown as LoDashStatic に変更
|
|
483
|
+
export default {
|
|
484
|
+
..._,
|
|
485
|
+
extend,
|
|
486
|
+
isEmptyOrg: _.isEmpty,
|
|
487
|
+
toNumberOrg: _.toNumber,
|
|
488
|
+
castArrayOrg: _.castArray,
|
|
489
|
+
isEmpty,
|
|
490
|
+
toNumber,
|
|
491
|
+
toBool,
|
|
492
|
+
boolIf,
|
|
493
|
+
isValidStr,
|
|
494
|
+
valueOr,
|
|
495
|
+
equalsOr,
|
|
496
|
+
emptyOr,
|
|
497
|
+
hasOr,
|
|
498
|
+
waited,
|
|
499
|
+
parseJSON,
|
|
500
|
+
jsonStringify,
|
|
501
|
+
castArray,
|
|
502
|
+
changes,
|
|
503
|
+
arrayDepth,
|
|
504
|
+
};
|
|
505
|
+
// 個別エクスポートはそのまま
|
|
506
|
+
export { isEmpty, toNumber, boolIf, isValidStr, valueOr, equalsOr, waited, parseJSON, jsonStringify, castArray, changes, arrayDepth, };
|