Files
frontend/apps/st/src/components/util/grid/computedFieldCallbacks.ts
T

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