cleaningRuleUtils.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. import dayjs from 'dayjs';
  2. import customParseFormat from 'dayjs/plugin/customParseFormat';
  3. dayjs.extend(customParseFormat);
  4. const SECOND_TIMESTAMP_REGEX = /^\d{10}(?:\.\d+)?$/;
  5. const MILLISECOND_TIMESTAMP_REGEX = /^\d{13}$/;
  6. const DATE_TIME_FORMATS = [
  7. 'YYYYMMDDHHmmss',
  8. 'YYYYMMDDHHmm',
  9. 'YYYY-MM-DD HH:mm:ss',
  10. 'YYYY-MM-DD HH:mm',
  11. 'YYYY/MM/DD HH:mm:ss',
  12. 'YYYY/MM/DD HH:mm',
  13. 'YYYY.MM.DD HH:mm:ss',
  14. 'YYYY.MM.DD HH:mm',
  15. ];
  16. const DATE_ONLY_FORMATS = ['YYYYMMDD', 'YYYY-MM-DD', 'YYYY/MM/DD', 'YYYY.MM.DD'];
  17. const TIME_ONLY_FORMATS = ['HHmmss', 'HH:mm:ss', 'HH.mm.ss', 'HH,mm,ss'];
  18. const PARTIAL_DATE_TIME_FORMATS = [
  19. 'YYYY-MM-DD HH:mm:ss',
  20. 'YYYY-MM-DD HH:mm',
  21. 'YYYY/MM/DD HH:mm:ss',
  22. 'YYYY/MM/DD HH:mm',
  23. 'YYYY.MM.DD HH:mm:ss',
  24. 'YYYY.MM.DD HH:mm',
  25. 'YYYY年MM月DD日 HH:mm:ss',
  26. 'YYYY年MM月DD日 HH:mm',
  27. ];
  28. const PARTIAL_DATE_ONLY_FORMATS = [
  29. 'YYYY-MM-DD',
  30. 'YYYY/MM/DD',
  31. 'YYYY.MM.DD',
  32. 'YYYY年MM月DD日',
  33. ];
  34. export type CleaningRuleKey =
  35. | 'ignoreLine'
  36. | 'dateConfig'
  37. | 'extractData'
  38. | 'extractSegment'
  39. | 'timestampConversion'
  40. | 'removeSpecialCharacters'
  41. | 'absoluteValue'
  42. | 'decimalConversion'
  43. | 'replaceValue'
  44. | 'groupingColumns'
  45. | 'magnificationAdjustment'
  46. | 'yearComplete'
  47. | 'usePreviousRowData';
  48. export interface CleaningRuleLike {
  49. key: CleaningRuleKey;
  50. enabled?: boolean;
  51. rules?: Record<string, any>;
  52. }
  53. export type PreviewRow = Record<string, string>;
  54. const tryParseByFormats = (value: string, formats: string[]) =>
  55. formats.map((format) => dayjs(value, format, true)).find((item) => item.isValid()) ?? null;
  56. const parseTimestampToDayjs = (value: string) => {
  57. if (SECOND_TIMESTAMP_REGEX.test(value)) {
  58. return dayjs(Number(value) * 1000);
  59. }
  60. if (MILLISECOND_TIMESTAMP_REGEX.test(value)) {
  61. return dayjs(Number(value));
  62. }
  63. return null;
  64. };
  65. const isPreviewRowSkipped = (rowIndex: number, headerRowIndex: number | null, ignoredRows: Set<number>) => {
  66. if (headerRowIndex === rowIndex) {
  67. return true;
  68. }
  69. return ignoredRows.has(rowIndex + 1);
  70. };
  71. export const splitValue = (value: unknown, separator: string, index?: number) => {
  72. const text = String(value ?? '');
  73. if (!text || !separator) {
  74. return [];
  75. }
  76. if (index && index > 0) {
  77. return text.split(separator)[index - 1];
  78. }
  79. return text.split(separator);
  80. };
  81. export const sliceValue = (value: unknown, start?: number, end?: number) => {
  82. const text = String(value ?? '');
  83. if (!text) {
  84. return '';
  85. }
  86. return text.slice(start, end);
  87. };
  88. export const getDateValue = (timestamp: unknown, withTime: boolean) => {
  89. const raw = String(timestamp ?? '').trim();
  90. if (!raw) {
  91. return raw;
  92. }
  93. const date = parseTimestampToDayjs(raw);
  94. if (!date || !date.isValid()) {
  95. return raw;
  96. }
  97. if (withTime) {
  98. return date.format('YYYY-MM-DD HH:mm:ss');
  99. }
  100. return date.format('YYYY-MM-DD');
  101. };
  102. export const convertDateValue = (value: unknown, type: number) => {
  103. const raw = String(value ?? '');
  104. const normalized = raw.trim();
  105. if (!normalized) {
  106. return '';
  107. }
  108. const time = tryParseByFormats(normalized, TIME_ONLY_FORMATS);
  109. if (time) {
  110. return type === 3 ? time.format('HH:mm:ss') : raw;
  111. }
  112. const date =
  113. tryParseByFormats(normalized, DATE_TIME_FORMATS) ||
  114. tryParseByFormats(normalized, DATE_ONLY_FORMATS) ||
  115. parseTimestampToDayjs(normalized);
  116. if (!date || !date.isValid()) {
  117. return raw;
  118. }
  119. const formatted = date.format('YYYY-MM-DD HH:mm:ss');
  120. if (type === 2) {
  121. return formatted.split(' ')[0];
  122. }
  123. if (type === 3) {
  124. return formatted.split(' ')[1];
  125. }
  126. return formatted;
  127. };
  128. export const replaceStrToBlankValue = (
  129. value: unknown,
  130. removeSpace: boolean,
  131. removeNewLine: boolean,
  132. removeText?: string,
  133. ) => {
  134. let text = String(value ?? '');
  135. if (!text) {
  136. return '';
  137. }
  138. if (removeSpace) {
  139. text = text.replace(/\s/g, '');
  140. }
  141. if (removeNewLine) {
  142. text = text.replace(/\r\n/g, '');
  143. }
  144. if (removeText) {
  145. text = text.split(String(removeText)).join('');
  146. }
  147. return text;
  148. };
  149. export const strToAbsoluteValue = (value: unknown, flag: unknown) => {
  150. if (!flag || flag === 'false') {
  151. return value;
  152. }
  153. const text = String(value ?? '');
  154. if (!/^-?\d+(\.\d+)?$/.test(text)) {
  155. return value;
  156. }
  157. return Math.abs(Number(text));
  158. };
  159. const toBinary = (value: number) => value.toString(2);
  160. const toOctal = (value: number) => value.toString(8);
  161. const toDecimal = (value: string | number) => parseInt(String(value), 10);
  162. const toHexadecimal = (value: number) => value.toString(16);
  163. export const decimalConversionFnValue = (value: unknown, targetBase: string, currentBase: string, flag: unknown) => {
  164. if (!flag || flag === 'false' || !targetBase || !currentBase) {
  165. return value;
  166. }
  167. const parsed = parseInt(String(value ?? ''), Number(currentBase));
  168. if (Number.isNaN(parsed)) {
  169. return value;
  170. }
  171. switch (targetBase) {
  172. case '2进制':
  173. return toBinary(parsed);
  174. case '8进制':
  175. return toOctal(parsed);
  176. case '10进制':
  177. return toDecimal(parsed);
  178. case '16进制':
  179. return toHexadecimal(parsed);
  180. default:
  181. return value;
  182. }
  183. };
  184. export const termReplaceValue = (
  185. value: unknown,
  186. source: string,
  187. target: string,
  188. term: string | number,
  189. flag: unknown,
  190. ) => {
  191. if (!flag || flag === 'false') {
  192. return value;
  193. }
  194. const text = String(value ?? '');
  195. const termValue = Number(term);
  196. if (termValue === 0) {
  197. return source && text.includes(source) ? text.split(source).join(target) : text;
  198. }
  199. if (termValue === 1) {
  200. return text === source ? target : text;
  201. }
  202. return text;
  203. };
  204. export const groupColumnsFnValue = (row: PreviewRow, groupColumns: Array<Record<string, any>>) => {
  205. return (groupColumns || []).reduce((result, item) => {
  206. const header = String(item?.header ?? '');
  207. const direction = String(item?.direction ?? '');
  208. const count = Number(item?.count ?? 0);
  209. const headerValue = String(row?.[header] ?? '');
  210. if (!headerValue || !count) {
  211. return result;
  212. }
  213. const segment =
  214. direction === '前'
  215. ? headerValue.substring(0, count)
  216. : headerValue.substring(Math.max(headerValue.length - count, 0));
  217. return result + segment;
  218. }, '');
  219. };
  220. export const multiplicationValue = (rawData: unknown, multiplier: number) => {
  221. if (rawData === undefined || rawData === null || rawData === '') {
  222. return rawData;
  223. }
  224. const decimalLength = String(rawData).split('.')?.[1]?.length || 0;
  225. return Number(Number(Number(rawData) * multiplier).toFixed(decimalLength));
  226. };
  227. export const yearCompleteFnValue = (rawData: unknown) => {
  228. const raw = String(rawData ?? '');
  229. const text = raw.trim();
  230. if (!text) {
  231. return rawData;
  232. }
  233. const currentYear = dayjs().year();
  234. const candidateText = `${currentYear}-${text}`;
  235. const date =
  236. tryParseByFormats(candidateText, PARTIAL_DATE_TIME_FORMATS) ||
  237. tryParseByFormats(candidateText, PARTIAL_DATE_ONLY_FORMATS);
  238. if (!date) {
  239. return rawData;
  240. }
  241. const normalizedDate = date.isAfter(dayjs()) ? date.subtract(1, 'year') : date;
  242. return /:\d{1,2}$/u.test(text) || text.includes(':')
  243. ? normalizedDate.format('YYYY-MM-DD HH:mm:ss')
  244. : normalizedDate.format('YYYY-MM-DD');
  245. };
  246. export const usePreviousRowDataFnValue = (rawData: unknown, list: unknown[], index: number) => {
  247. let value = rawData;
  248. let rowIndex = index;
  249. while (!value && rowIndex > 1) {
  250. value = list[rowIndex - 1];
  251. rowIndex -= 1;
  252. }
  253. return value;
  254. };
  255. export const resolveSplitDelimiter = (delimiterType: string, delimiterValue = '') => {
  256. if (delimiterType === 'comma') {
  257. return ',';
  258. }
  259. if (delimiterType === 'space') {
  260. return ' ';
  261. }
  262. return delimiterValue;
  263. };
  264. export const normalizeDecimalBaseLabel = (value: unknown) => {
  265. const text = String(value ?? '').trim();
  266. if (['2', '2进制'].includes(text)) {
  267. return '2进制';
  268. }
  269. if (['8', '8进制'].includes(text)) {
  270. return '8进制';
  271. }
  272. if (['10', '10进制'].includes(text)) {
  273. return '10进制';
  274. }
  275. if (['16', '16进制'].includes(text)) {
  276. return '16进制';
  277. }
  278. return text;
  279. };
  280. export const escapeRegExpText = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  281. export const buildRemoveSpecialRegex = (specialList: string[] = [], specialStr = '') => {
  282. const patterns: string[] = [];
  283. if (specialList.includes('移除空格字符')) {
  284. patterns.push('\\s');
  285. }
  286. if (specialList.includes('移除换行字符(tab)')) {
  287. patterns.push('\\r\\n');
  288. }
  289. if (specialList.includes('要移除的特殊字符') && specialStr) {
  290. patterns.push(escapeRegExpText(specialStr));
  291. }
  292. if (!patterns.length) {
  293. return '';
  294. }
  295. return patterns.length === 1 ? patterns[0] : `(?:${patterns.join('|')})`;
  296. };
  297. interface RunPreviewPipelineParams {
  298. rows: PreviewRow[];
  299. headers: string[];
  300. columnRules: CleaningRuleLike[][];
  301. ignoredRows: Set<number>;
  302. headerRowIndex: number | null;
  303. }
  304. export const runCleaningPreviewPipeline = ({
  305. rows,
  306. headers,
  307. columnRules,
  308. ignoredRows,
  309. headerRowIndex,
  310. }: RunPreviewPipelineParams) => {
  311. const nextRows = JSON.parse(JSON.stringify(rows)) as PreviewRow[];
  312. headers.forEach((header, columnIndex) => {
  313. const rules = columnRules[columnIndex] || [];
  314. if (!rules.length) {
  315. return;
  316. }
  317. rules.forEach((rule) => {
  318. if (rule.enabled === false) {
  319. return;
  320. }
  321. switch (rule.key) {
  322. case 'extractSegment': {
  323. const separator = String(rule.rules?.segment ?? '');
  324. const segmentRow = Number(rule.rules?.segmentRow ?? 1) || 1;
  325. nextRows.forEach((row, rowIndex) => {
  326. if (isPreviewRowSkipped(rowIndex, headerRowIndex, ignoredRows)) {
  327. return;
  328. }
  329. row[header] = String(splitValue(row[header], separator, segmentRow) ?? '');
  330. });
  331. break;
  332. }
  333. case 'extractData': {
  334. const beforeModel = Number(rule.rules?.beforeModel ?? 0);
  335. const nextModel = Number(rule.rules?.nextModel ?? 0);
  336. if (beforeModel) {
  337. nextRows.forEach((row, rowIndex) => {
  338. if (isPreviewRowSkipped(rowIndex, headerRowIndex, ignoredRows)) {
  339. return;
  340. }
  341. row[header] = String(sliceValue(row[header], 0, beforeModel) ?? '');
  342. });
  343. }
  344. if (nextModel) {
  345. nextRows.forEach((row, rowIndex) => {
  346. if (isPreviewRowSkipped(rowIndex, headerRowIndex, ignoredRows)) {
  347. return;
  348. }
  349. row[header] = String(sliceValue(row[header], -nextModel) ?? '');
  350. });
  351. }
  352. break;
  353. }
  354. case 'timestampConversion': {
  355. nextRows.forEach((row, rowIndex) => {
  356. if (isPreviewRowSkipped(rowIndex, headerRowIndex, ignoredRows)) {
  357. return;
  358. }
  359. row[header] = String(getDateValue(row[header], true) ?? '');
  360. });
  361. break;
  362. }
  363. case 'dateConfig': {
  364. const dateType = Number(rule.rules?.dateType ?? 1) || 1;
  365. nextRows.forEach((row, rowIndex) => {
  366. if (isPreviewRowSkipped(rowIndex, headerRowIndex, ignoredRows)) {
  367. return;
  368. }
  369. row[header] = String(convertDateValue(row[header], dateType) ?? '');
  370. });
  371. break;
  372. }
  373. case 'removeSpecialCharacters': {
  374. const specialList = Array.isArray(rule.rules?.specialList) ? rule.rules.specialList : [];
  375. const specialStr = String(rule.rules?.specialStr ?? '');
  376. const removeSpace = specialList.includes('移除空格字符');
  377. const removeNewLine = specialList.includes('移除换行字符(tab)');
  378. const removeCustom = specialList.includes('要移除的特殊字符') ? specialStr : '';
  379. nextRows.forEach((row, rowIndex) => {
  380. if (isPreviewRowSkipped(rowIndex, headerRowIndex, ignoredRows)) {
  381. return;
  382. }
  383. row[header] = String(replaceStrToBlankValue(row[header], removeSpace, removeNewLine, removeCustom) ?? '');
  384. });
  385. break;
  386. }
  387. case 'absoluteValue': {
  388. const absoluteFlag = rule.rules?.absoluteData;
  389. nextRows.forEach((row, rowIndex) => {
  390. if (isPreviewRowSkipped(rowIndex, headerRowIndex, ignoredRows)) {
  391. return;
  392. }
  393. row[header] = String(strToAbsoluteValue(row[header], absoluteFlag) ?? '');
  394. });
  395. break;
  396. }
  397. case 'decimalConversion': {
  398. const decimalFlag = rule.rules?.decimalData;
  399. const targetBase = normalizeDecimalBaseLabel(rule.rules?.decimalModel);
  400. const currentBase = normalizeDecimalBaseLabel(rule.rules?.decimalModel2).replace('进制', '');
  401. nextRows.forEach((row, rowIndex) => {
  402. if (isPreviewRowSkipped(rowIndex, headerRowIndex, ignoredRows)) {
  403. return;
  404. }
  405. row[header] = String(decimalConversionFnValue(row[header], targetBase, currentBase, decimalFlag) ?? '');
  406. });
  407. break;
  408. }
  409. case 'replaceValue': {
  410. const replaceFlag = rule.rules?.replaceFlag;
  411. const sourceData = String(rule.rules?.sourceData ?? '');
  412. const targetData = String(rule.rules?.targetData ?? '');
  413. const replaceModel = rule.rules?.replaceModel ?? 0;
  414. nextRows.forEach((row, rowIndex) => {
  415. if (isPreviewRowSkipped(rowIndex, headerRowIndex, ignoredRows)) {
  416. return;
  417. }
  418. row[header] = String(
  419. termReplaceValue(row[header], sourceData, targetData, replaceModel, replaceFlag) ?? '',
  420. );
  421. });
  422. break;
  423. }
  424. case 'groupingColumns': {
  425. const groupColumns = Array.isArray(rule.rules?.groupColumns)
  426. ? rule.rules.groupColumns.filter((item) => {
  427. const header = String(item?.header ?? '').trim();
  428. const direction = String(item?.direction ?? '').trim();
  429. const count = Number(item?.count ?? 0);
  430. return !!header && !!direction && count > 0;
  431. })
  432. : [];
  433. if (!groupColumns.length) {
  434. break;
  435. }
  436. nextRows.forEach((row, rowIndex) => {
  437. if (isPreviewRowSkipped(rowIndex, headerRowIndex, ignoredRows)) {
  438. return;
  439. }
  440. row[header] = String(groupColumnsFnValue(nextRows[rowIndex], groupColumns) ?? '');
  441. });
  442. break;
  443. }
  444. case 'magnificationAdjustment': {
  445. const factor = Number(rule.rules?.magnificationAdjustmentValue ?? 1) || 1;
  446. nextRows.forEach((row, rowIndex) => {
  447. if (isPreviewRowSkipped(rowIndex, headerRowIndex, ignoredRows)) {
  448. return;
  449. }
  450. row[header] = String(multiplicationValue(row[header], factor) ?? '');
  451. });
  452. break;
  453. }
  454. case 'yearComplete': {
  455. nextRows.forEach((row, rowIndex) => {
  456. if (isPreviewRowSkipped(rowIndex, headerRowIndex, ignoredRows)) {
  457. return;
  458. }
  459. row[header] = String(yearCompleteFnValue(row[header]) ?? '');
  460. });
  461. break;
  462. }
  463. case 'usePreviousRowData': {
  464. const dataList = nextRows.map((item) => item[header]);
  465. nextRows.forEach((row, rowIndex) => {
  466. if (isPreviewRowSkipped(rowIndex, headerRowIndex, ignoredRows)) {
  467. return;
  468. }
  469. row[header] = String(usePreviousRowDataFnValue(row[header], dataList, rowIndex) ?? '');
  470. });
  471. break;
  472. }
  473. default:
  474. break;
  475. }
  476. });
  477. });
  478. return nextRows;
  479. };