import type { DataFieldValueCallback, ComputedFieldConfig } from "@/components/type/DataGrid.type"; /** 값 → 숫자 변환 (null/undefined/빈문자열 → 0) */ function toNum(v: unknown): number { if (v == null || v === "") return 0; if (typeof v === "number") return isNaN(v) ? 0 : v; const n = parseFloat(String(v).replace(/,/g, "")); return isNaN(n) ? 0 : n; } /** 소수점 첫째 자리 이후 버림 (반올림 없음) */ function truncateToFirstDecimal(value: number): number { if (!Number.isFinite(value)) return 0; return Math.floor(value * 10) / 10; } /** computed sum용 스케일 (표시 최대 소수 3자리 + 여유) */ const SUM_DECIMAL_SCALE = 1e6; /** * 스케일 정수 합을 Number로 되돌릴 때 생기는 IEEE 오차 보정. * 예: 9003630000/1e6 → 9003.62999… (RealGrid trunc 시 9003.62) * 아주 작은 양수를 더해 표시가 9003.63이 되게 한다. */ function numberFromScaledSum(scaledSum: number): number { if (scaledSum === 0) return 0; const raw = scaledSum / SUM_DECIMAL_SCALE; if (scaledSum % SUM_DECIMAL_SCALE === 0) return raw; return raw + Math.sign(raw) * 1e-12; } /** * 각 값을 소수 6자리 정수로 올린 뒤 더한 다음 다시 나눈다. * 예: 2712.5 + 6291.13 → (2712500000 + 6291130000) / 1e6 = 9003.63 */ function sumViaScaledIntegers(nums: number[]): number { let scaledSum = 0; for (const n of nums) { if (!Number.isFinite(n)) continue; scaledSum += Math.round(n * SUM_DECIMAL_SCALE); } return numberFromScaledSum(scaledSum); } /** 합계용 valueCallback 생성 */ export function createSumValueCallback(fieldNames: string[]): DataFieldValueCallback { return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { const nums: number[] = []; for (const f of fieldNames) { const idx = fieldNamesArr.indexOf(f); if (idx >= 0) nums.push(toNum(values[idx])); } return sumViaScaledIntegers(nums); }; } /** 평균용 valueCallback 생성 */ export function createAvgValueCallback(fieldNames: string[]): DataFieldValueCallback { return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { const nums: number[] = []; for (const f of fieldNames) { const idx = fieldNamesArr.indexOf(f); if (idx >= 0) { nums.push(toNum(values[idx])); } } if (nums.length === 0) return 0; return sumViaScaledIntegers(nums) / nums.length; }; } function sumFieldValues( fields: string[], fieldNamesArr: string[], values: unknown[], ): number { const nums: number[] = []; for (const f of fields) { const idx = fieldNamesArr.indexOf(f); if (idx >= 0) nums.push(toNum(values[idx])); } return sumViaScaledIntegers(nums); } /** halfSumOfSums 한 건 — (sum(first) + sum(second)) / 2, 소수 내림 */ function computeHalfSumOfSums( firstSumFields: string[], secondSumFields: string[], fieldNamesArr: string[], values: unknown[], ): number { const firstSum = sumFieldValues(firstSumFields, fieldNamesArr, values); const secondSum = sumFieldValues(secondSumFields, fieldNamesArr, values); return Math.floor((firstSum + secondSum) / 2); } export type HalfSumOfSumsGroup = { firstSumFields: string[]; secondSumFields: string[]; }; /** 두 그룹 합계의 평균 — (sum(first) + sum(second)) / 2 */ export function createHalfSumOfSumsValueCallback( firstSumFields: string[], secondSumFields: string[], ): DataFieldValueCallback { return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => computeHalfSumOfSums(firstSumFields, secondSumFields, fieldNamesArr, values); } /** halfSumOfSums 2건의 합 — (B+C)/2 + (E+F)/2 */ export function createSumOfTwoHalfSumOfSumsValueCallback( firstGroup: HalfSumOfSumsGroup, secondGroup: HalfSumOfSumsGroup, ): DataFieldValueCallback { return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => computeHalfSumOfSums( firstGroup.firstSumFields, firstGroup.secondSumFields, fieldNamesArr, values, ) + computeHalfSumOfSums( secondGroup.firstSumFields, secondGroup.secondSumFields, fieldNamesArr, values, ); } /** 차이값: fields[0] − fields[1] (예: 매설연장 − 철거연장) */ export function createDifferenceValueCallback(fields: string[]): DataFieldValueCallback { const a = fields[0] ?? ""; const b = fields[1] ?? ""; return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { if (!a || !b) return 0; const ia = fieldNamesArr.indexOf(a); const ib = fieldNamesArr.indexOf(b); const va = ia >= 0 ? toNum(values[ia]) : 0; const vb = ib >= 0 ? toNum(values[ib]) : 0; return va - vb; }; } /** (여러 필드 합) - (여러 필드 합) */ export function createDifferenceOfSumsValueCallback( minuendFields: string[], subtrahendFields: string[], ): DataFieldValueCallback { return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { const sumOf = (fields: string[]) => { let sum = 0; for (const f of fields) { const idx = fieldNamesArr.indexOf(f); if (idx >= 0) sum += toNum(values[idx]); } return sum; }; return sumOf(minuendFields) - sumOf(subtrahendFields); }; } /** 최대값용 valueCallback 생성 */ export function createMaxValueCallback(fieldNames: string[]): DataFieldValueCallback { return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { let max = Number.NEGATIVE_INFINITY; for (const f of fieldNames) { const idx = fieldNamesArr.indexOf(f); if (idx >= 0) { const val = toNum(values[idx]); max = Math.max(max, val); } } return max === Number.NEGATIVE_INFINITY ? 0 : max; }; } /** 단일 필드값 vs 여러 필드 합계 중 최대값 */ export function createMaxOfFieldAndSumValueCallback( baseField: string, sumFields: string[], ): DataFieldValueCallback { return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { const baseIdx = fieldNamesArr.indexOf(baseField); const baseVal = baseIdx >= 0 ? toNum(values[baseIdx]) : 0; let sumVal = 0; for (const f of sumFields) { const idx = fieldNamesArr.indexOf(f); if (idx >= 0) sumVal += toNum(values[idx]); } return Math.max(baseVal, sumVal); }; } /** 여러 그룹의 max(baseField, sum(sumFields)) 값을 모두 합산 */ export function createSumOfMaxOfFieldAndSumGroupsValueCallback( groups: Array<{ baseField: string; sumFields: string[] }>, ): DataFieldValueCallback { return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { let total = 0; for (const group of groups) { const baseIdx = fieldNamesArr.indexOf(group.baseField); const baseVal = baseIdx >= 0 ? toNum(values[baseIdx]) : 0; let sumVal = 0; for (const f of group.sumFields) { const idx = fieldNamesArr.indexOf(f); if (idx >= 0) sumVal += toNum(values[idx]); } total += Math.max(baseVal, sumVal); } return total; }; } // 공공하수처리구역인구보급률(%), 고도처리인구보급률(%) valueCallback export function createCoverageRateValueCallback( numeratorFields: string[], denominatorFields: string[] ): DataFieldValueCallback { return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { const getSum = (fields: string[]) => { let sum = 0; for (const f of fields) { const idx = fieldNamesArr.indexOf(f); if (idx >= 0) sum += toNum(values[idx]); } return sum; }; const total2 = getSum(numeratorFields); const total1 = getSum(denominatorFields); return total1 === 0 ? 0 : (total2 / total1) * 100; }; } // 하수도 설치율(%) valueCallback — 항목·중간·결과 모두 소수 첫째 자리 이후 버림(반올림 없음) export function createSwrgInstlrtRateValueCallback( numeratorFields: string[], denominatorFields: string[], thirdFields: string[], ): DataFieldValueCallback { return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { const getSum = (fields: string[]) => { let sum = 0; for (const f of fields) { const idx = fieldNamesArr.indexOf(f); if (idx >= 0) { sum += truncateToFirstDecimal(toNum(values[idx])); } } return truncateToFirstDecimal(sum); }; const sum1 = getSum(numeratorFields); const sum2 = getSum(denominatorFields); const piplSupprt = truncateToFirstDecimal( toNum(values[fieldNamesArr.indexOf(thirdFields[0] || "piplSupprt")]), ); const ratioPercent = sum1 === 0 ? 0 : truncateToFirstDecimal((sum2 / sum1) * 100); const result = truncateToFirstDecimal((ratioPercent * piplSupprt) / 100); return result; }; } /** 처리효율: (유입BOD − 방류BOD) / 유입BOD */ export function createBodPrcsRtValueCallback( inflwField: string, dschgField: string, ): DataFieldValueCallback { return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { const inflwIdx = fieldNamesArr.indexOf(inflwField); const dschgIdx = fieldNamesArr.indexOf(dschgField); const inflw = inflwIdx >= 0 ? toNum(values[inflwIdx]) : 0; const dschg = dschgIdx >= 0 ? toNum(values[dschgIdx]) : 0; if (inflw === 0) return 0; return (inflw - dschg) / inflw; }; } /** 총양수량: Σ(펌프대수 + 예비펌프대수) × 대당 양수량 */ export function createPumpGrossPpqtyValueCallback( pumpGroups: Array<{ pumpCntField: string; rsrvPumpCntField: string; perPpqtyField: string; }>, ): DataFieldValueCallback { return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { let total = 0; for (const group of pumpGroups) { const pumpIdx = fieldNamesArr.indexOf(group.pumpCntField); const rsrvIdx = fieldNamesArr.indexOf(group.rsrvPumpCntField); const perIdx = fieldNamesArr.indexOf(group.perPpqtyField); const pumpCnt = pumpIdx >= 0 ? toNum(values[pumpIdx]) : 0; const rsrvCnt = rsrvIdx >= 0 ? toNum(values[rsrvIdx]) : 0; const perPpqty = perIdx >= 0 ? toNum(values[perIdx]) : 0; total += (pumpCnt + rsrvCnt) * perPpqty; } return total; }; } /** (a/b) / (c/d) × 100 — 각 quotient 분모 0이면 0 */ export function createQuotientRatioPercentValueCallback( numeratorQuotient: [string, string], denominatorQuotient: [string, string], ): DataFieldValueCallback { const [numField, numDenField] = numeratorQuotient; const [denNumField, denDenField] = denominatorQuotient; return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { const getVal = (field: string) => { const idx = fieldNamesArr.indexOf(field); return idx >= 0 ? toNum(values[idx]) : 0; }; const numDen = getVal(numDenField); const denDen = getVal(denDenField); const denNum = getVal(denNumField); if (numDen === 0 || denDen === 0 || denNum === 0) return 0; const numerator = getVal(numField) / numDen; const denominator = denNum / denDen; if (denominator === 0) return 0; return (numerator / denominator) * 100; }; } /** Σ(numeratorFields) / denominatorField × 10,000 — 기준인구당 건수(건/10,000명) */ export function createSumPerTenThousandValueCallback( numeratorFields: string[], denominatorField: string, ): DataFieldValueCallback { return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { const sum = sumFieldValues(numeratorFields, fieldNamesArr, values); const denIdx = fieldNamesArr.indexOf(denominatorField); const den = denIdx >= 0 ? toNum(values[denIdx]) : 0; if (den === 0) return 0; return truncateToFirstDecimal((sum / den) * 10000); }; } /** 나눗셈: fields[0] / fields[1] (분모 0이면 0) */ export function createQuotientValueCallback(fields: string[]): DataFieldValueCallback { const numeratorField = fields[0] ?? ""; const denominatorField = fields[1] ?? ""; return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { if (!numeratorField || !denominatorField) return 0; const numIdx = fieldNamesArr.indexOf(numeratorField); const denIdx = fieldNamesArr.indexOf(denominatorField); const numerator = numIdx >= 0 ? toNum(values[numIdx]) : 0; const denominator = denIdx >= 0 ? toNum(values[denIdx]) : 0; if (denominator === 0) return 0; return numerator / denominator; }; } /** (numerator / denominator) × multiplier × 100 — 분모 0이면 0 */ export function createQuotientTimesMultiplierPercentValueCallback( numeratorField: string, denominatorField: string, multiplierField: string, ): DataFieldValueCallback { return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { const getVal = (field: string) => { const idx = fieldNamesArr.indexOf(field); return idx >= 0 ? toNum(values[idx]) : 0; }; const denominator = getVal(denominatorField); if (denominator === 0) return 0; const quotient = getVal(numeratorField) / denominator; const multiplier = getVal(multiplierField); return truncateToFirstDecimal(quotient * multiplier * 100); }; } /** 연산 타입에 맞는 valueCallback 반환 */ export function getComputedValueCallback(config: ComputedFieldConfig): DataFieldValueCallback { switch (config.type) { case 'sum': return createSumValueCallback(config.fields); case 'avg': return createAvgValueCallback(config.fields); case 'max': return createMaxValueCallback(config.fields); case 'difference': return createDifferenceValueCallback(config.fields); case 'quotient': return createQuotientValueCallback(config.fields); case 'quotientRatioPercent': return createQuotientRatioPercentValueCallback( config.numeratorQuotient, config.denominatorQuotient, ); case 'differenceOfSums': return createDifferenceOfSumsValueCallback( config.minuendFields, config.subtrahendFields, ); case 'halfSumOfSums': return createHalfSumOfSumsValueCallback( config.firstSumFields, config.secondSumFields, ); case 'sumOfTwoHalfSumOfSums': return createSumOfTwoHalfSumOfSumsValueCallback( config.firstHalfSum, config.secondHalfSum, ); case 'maxOfFieldAndSum': return createMaxOfFieldAndSumValueCallback( config.baseField, config.sumFields, ); case 'sumOfMaxOfFieldAndSumGroups': return createSumOfMaxOfFieldAndSumGroupsValueCallback( config.groups, ); case 'pblswrgPopltnSuprtRate': case 'adtrtPopltnSuprtRate': return createCoverageRateValueCallback(config.numeratorFields, config.denominatorFields); case 'swrgInstlrtRate': return createSwrgInstlrtRateValueCallback(config.numeratorFields, config.denominatorFields, config.thirdFields); case 'pumpGrossPpqty': return createPumpGrossPpqtyValueCallback(config.pumpGroups); case 'bodPrcsRt': return createBodPrcsRtValueCallback(config.inflwField, config.dschgField); case 'sumPerTenThousand': return createSumPerTenThousandValueCallback( config.numeratorFields, config.denominatorField, ); case 'quotientTimesMultiplierPercent': return createQuotientTimesMultiplierPercentValueCallback( config.numeratorField, config.denominatorField, config.multiplierField, ); } }