Files
AllinSSL/frontend/packages/utils/src/data.ts
2026-01-13 17:47:39 +08:00

418 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 文件定义:数据处理方法
* 包含1、数据类型检查。2、数据转换。3、日期处理。4、数据校验。5、数据过滤与重组。6、特殊场景处理
*/
import * as R from "ramda";
// =============== 数据转换 ===============
/**
* 数字格式化
* @param num 数字
* @param decimals 小数位数
* @param thousandsSeparator 千位分隔符
* @param decimalSeparator 小数分隔符
*/
export const formatNumber = (
num: number | string,
decimals = 0,
thousandsSeparator = ",",
decimalSeparator = "."
): string => {
const number = Number(num);
if (isNaN(number)) return "";
const fixed = number.toFixed(decimals);
const parts = fixed.split(".");
// 添加千位分隔符
if (parts[0]) {
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSeparator);
}
return parts.join(decimalSeparator);
};
/**
* 货币格式化
* @param amount 金额
* @param currency 货币符号
* @param decimals 小数位数
*/
export const formatCurrency = (
amount: number | string,
currency = "¥",
decimals = 2
): string => {
const formatted = formatNumber(amount, decimals);
return `${currency}${formatted}`;
};
/**
* 将对象的所有值转换为字符串
* @param {Record<string, any>} obj - 要转换的对象
* @returns {Record<string, string>} 转换后的对象
*/
export const objectToString = R.map(String);
/**
* 将数组转换为对象,使用指定的 key
* @param {string} key - 要转换的 key
* @param {Record<string, any>[]} array - 要转换的数组
* @returns {Record<string, Record<string, any>>} 转换后的对象
*/
export const arrayToObject = R.curry(
(key: string, array: Record<string, any>[]) => R.indexBy(R.prop(key), array)
) as <T extends Record<string, any>>(
key: string,
array: T[]
) => Record<string, T>;
/**
* 深度扁平化对象(建议深度嵌套的对象使用)
* @param {Record<string, any>} obj - 要扁平化的对象
* @returns {Record<string, any>} 扁平化后的对象
*/
export const flattenObject = (
obj: Record<string, unknown>
): Record<string, unknown> => {
const result: Record<string, unknown> = {};
const flatten = (obj: Record<string, any>, prefix: string = "") => {
for (const key in obj) {
const value = obj[key];
const newKey = prefix ? `${prefix}.${key}` : key;
if (value && typeof value === "object" && !Array.isArray(value)) {
flatten(value, newKey);
} else {
result[newKey] = value;
}
}
};
flatten(obj);
return result;
};
/**
* 验证字符串是否符合正则表达式
* @param {RegExp} pattern - 要验证的正则表达式
* @param {string} str - 要验证的字符串
* @returns {boolean} 如果字符串符合正则表达式,则返回 true否则返回 false
*/
export const matchesPattern = R.curry((pattern: RegExp, str: string) =>
R.test(pattern, str)
) as <T extends RegExp>(pattern: T, str: string) => boolean;
/**
* 验证对象是否包含所有必需的键
* @param {Record<string, any>} obj - 要验证的对象
* @param {string[]} requiredKeys - 要验证的键
* @returns {boolean} 如果对象包含所有必需的键,则返回 true否则返回 false
*/
export const hasRequiredKeys = R.curry(
(obj: Record<string, unknown>, requiredKeys: string[]) =>
R.all(R.flip(R.has)(obj), requiredKeys)
) as {
(obj: Record<string, unknown>): (requiredKeys: string[]) => boolean;
(obj: Record<string, unknown>, requiredKeys: string[]): boolean;
};
// ... existing code ...
/**
* 验证值是否在指定范围内
* @param {number} min - 最小值
* @param {number} max - 最大值
* @param {number} value - 要验证的值
* @returns {boolean} 如果值在指定范围内,则返回 true否则返回 false
*/
export const isInRange = R.curry((min: number, max: number, value: number) =>
R.both(R.gte(R.__, min), R.lte(R.__, max))(value)
) as <T extends number>(min: T, max: T, value: T) => boolean;
// =============== 数据过滤与重组 ===============
/**
* 根据条件过滤对象的属性
* @param {Function} predicate - 要过滤的条件
* @param {Record<string, any>} obj - 要过滤的对象
* @returns {Record<string, any>} 过滤后的对象
*/
export const filterObject = R.curry(
<T extends Record<string, any>>(
predicate: (value: T[keyof T]) => boolean,
obj: T
) =>
Object.fromEntries(
Object.entries(obj).filter(([_, value]) => predicate(value))
)
) as {
<T extends Record<string, any>>(predicate: (value: T[keyof T]) => boolean): (
obj: T
) => Partial<T>;
<T extends Record<string, any>>(
predicate: (value: T[keyof T]) => boolean,
obj: T
): Partial<T>;
};
/**
* 按照指定的键对数组进行分组
* @param {string} key - 要分组的键
* @param {Record<string, any>[]} array - 要分组的数组
* @returns {Record<string, Record<string, any>[]>} 分组后的对象
*/
export const groupByKey = R.curry(
<T extends Record<string, any>>(key: string, array: T[]) =>
R.groupBy(R.prop(key), array)
) as <T extends Record<string, any>>(
key: string,
array: T[]
) => Record<string, T[]>;
/**
* 从对象数组中提取指定的键值
* @param {string[]} path - 要提取的键
* @param {Record<string, any>[]} list - 要提取的对象数组
* @returns {Record<string, any>[]} 提取后的对象数组
*/
export const pluckDeep = R.curry(<T>(path: string[], list: T[]) =>
R.map(R.path(path), list)
) as <T extends Record<string, any>>(path: string[], list: T[]) => T[];
/**
* 对嵌套数组进行扁平化和去重
* @param {any[]} array - 要扁平化和去重的数组
* @returns {any[]} 扁平化和去重后的数组
*/
export const flattenAndUniq = R.pipe(R.flatten, R.uniq) as <T>(
array: T[]
) => T[];
// =============== 数据映射 ===============
type MapperOption = {
inherit?: string[]; // 继承字段
deep?: boolean; // 深度映射
ignore?: string[]; // 忽略字段
};
type MapperType = [string, string][] | Record<string, string>;
type DataType = Record<string, unknown> | Record<string, unknown>[];
/**
* 对象/数组映射,根据映射表,将数组或对象映射为新的对象和数组
* 支持继承/过滤,通过参数继承/过滤,选取自己需要的数据
* 增加异常处理,如果值不存在,则抛出异常。
* 返回新的对象/数组
*/
export const mapData = (
mapper: MapperType,
data: DataType,
options: MapperOption = { deep: true }
): DataType => {
const { inherit, deep, ignore } = options;
// 验证 inherit 和 ignore 不能同时使用
if (inherit && ignore) {
throw new Error("inherit 和 ignore 选项不能同时使用");
}
// 将 mapper 转换为对象形式
const mapperObj = Array.isArray(mapper)
? mapper.reduce<Record<string, string>>(
(acc, [key, value]) => ({ ...acc, [key]: value }),
{}
)
: mapper;
// 处理数组
if (Array.isArray(data)) {
return data.map(
(item) => mapData(mapperObj, item, options) as Record<string, unknown>
);
}
// 处理对象
if (typeof data === "object" && data !== null) {
// 根据选项过滤 mapper
let finalMapper = { ...mapperObj };
if (inherit) {
finalMapper = Object.entries(mapperObj)
.filter(([key]) => inherit.includes(key))
.reduce<Record<string, string>>(
(acc, [key, value]) => ({ ...acc, [key]: value }),
{}
);
} else if (ignore) {
finalMapper = Object.entries(mapperObj)
.filter(([key]) => !ignore.includes(key))
.reduce<Record<string, string>>(
(acc, [key, value]) => ({ ...acc, [key]: value }),
{}
);
}
return Object.entries(finalMapper).reduce<Record<string, unknown>>(
(result, [sourceKey, targetKey]) => {
// 处理嵌套路径
const value = sourceKey.split(".").reduce<unknown>((obj, key) => {
if (obj === undefined || obj === null) {
throw new Error(`映射键 "${sourceKey}" 不存在于源数据中`);
}
return (obj as Record<string, unknown>)[key];
}, data);
// 处理值不存在的情况
if (value === undefined) {
throw new Error(`映射键 "${sourceKey}" 的值不存在`);
}
// 处理深度映射
if (deep && typeof value === "object" && value !== null) {
const nestedMapper = Object.entries(mapperObj)
.filter(([key]) => key.startsWith(`${sourceKey}.`))
.reduce<Record<string, string>>(
(acc, [key, val]) => ({
...acc,
[key.slice(sourceKey.length + 1)]: val,
}),
{}
);
if (Object.keys(nestedMapper).length > 0) {
return {
...result,
[targetKey]: mapData(
nestedMapper,
value as Record<string, unknown>,
options
),
};
}
}
// 处理嵌套目标路径
const targetPath = (targetKey as string).split(".");
const finalKey = targetPath.pop()!;
const targetObj = targetPath.reduce<Record<string, unknown>>(
(obj, key) => {
if (!(key in obj)) {
obj[key] = {};
}
return obj[key] as Record<string, unknown>;
},
result
);
if (finalKey && targetObj) {
targetObj[finalKey] = value;
}
return result;
},
{}
);
}
return data;
};
/**
* @description 生成映射表,将所有字段转换为小驼峰
* @param {Record<string, unknown>} obj - 要转换的对象
* @returns {Record<string, unknown>} 转换后的对象
*/
export const generateMapper = (obj: Record<string, unknown>) => {
return Object.entries(obj).map(([key, value]) => [
key.toLowerCase().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()),
value,
]);
};
/**
* 将对象转换为查询字符串
* @param {Record<string, any>} obj - 要转换的对象
* @returns {string} 转换后的查询字符串
*/
export const objectToQueryString = (obj: Record<string, any>) => {
return Object.entries(obj)
.map(
([key, value]) =>
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`
)
.join("&");
};
/**
* 深度合并两个对象
* @param {Record<string, any>} target - 目标对象
* @param {Record<string, any>} source - 源对象
* @returns {Record<string, any>} 合并后的对象
*/
export const deepMerge = <T extends Record<string, any>>(
target: T,
source: T,
isMergeArray: boolean = true
): T => {
const result = { ...target } as T;
for (const key in source) {
if (source.hasOwnProperty(key)) {
const sourceValue = source[key];
const targetValue = target[key];
if (Array.isArray(sourceValue) && Array.isArray(targetValue)) {
// 如果是数组,则合并数组
result[key] = (
isMergeArray ? [...targetValue, ...sourceValue] : sourceValue
) as T[Extract<keyof T, string>];
} else if (isObject(sourceValue) && isObject(targetValue)) {
// 如果是对象,则递归合并
result[key] = deepMerge(targetValue, sourceValue) as T[Extract<
keyof T,
string
>];
} else {
// 其他情况直接覆盖
result[key] = sourceValue as T[Extract<keyof T, string>];
}
}
}
return result;
};
/**
* 判断是否为对象
* @param {any} value - 要判断的值
* @returns {boolean} 是否为对象
*/
const isObject = (value: any): boolean => {
return value !== null && typeof value === "object" && !Array.isArray(value);
};
/**
* @description 清理对象前后字符串
* @param {Record<string, any>} obj - 要清理的对象
* @returns {Record<string, any>} 清理后的对象
*/
export const trimObject = (obj: Record<string, any>) => {
return Object.entries(obj).reduce<Record<string, any>>(
(acc, [key, value]) => {
acc[key.trim()] = value.trim();
return acc;
},
{}
);
};
/**
* 深拷贝对象(简单版)
* @param {any} obj - 要拷贝的对象
* @returns {any} 拷贝后的对象
*/
export const deepClone = <T>(obj: T): T => {
return JSON.parse(JSON.stringify(obj));
};