440 lines
16 KiB
TypeScript
440 lines
16 KiB
TypeScript
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,
|
||
);
|
||
}
|
||
}
|