refactor: centralize lake metrics calculations into a utility module with comprehensive unit tests

This commit is contained in:
David Fencl
2026-06-06 11:45:56 +02:00
parent 6d77c20c84
commit dbb22e7972
4 changed files with 130 additions and 20 deletions
+70
View File
@@ -0,0 +1,70 @@
import { describe, it, expect } from 'vitest';
import { calculateLakeMetrics, LakeCalculationConfig } from '../utils/calculations';
describe('calculateLakeMetrics', () => {
const config: LakeCalculationConfig = {
minLevel: 100,
maxLevel: 110,
storageLevel: 108,
maxVolume: 50,
};
it('should calculate capacity based on reported volume when available', () => {
// 25 / 50 = 50%
const result = calculateLakeMetrics(105, 25, config);
expect(result.capacity).toBe(50);
expect(result.volume).toBe(25);
});
it('should cap capacity at 100% when volume exceeds maxVolume', () => {
const result = calculateLakeMetrics(111, 55, config);
expect(result.capacity).toBe(100);
expect(result.volume).toBe(55);
});
it('should floor capacity at 0% when volume is negative', () => {
const result = calculateLakeMetrics(99, -5, config);
expect(result.capacity).toBe(0);
expect(result.volume).toBe(-5);
});
it('should estimate capacity and volume from level when reported volume is 0', () => {
// Level 105 is exactly halfway between 100 and 110 -> 50%
// 50% of 50 maxVolume = 25
const result = calculateLakeMetrics(105, 0, config);
expect(result.capacity).toBe(50);
expect(result.volume).toBe(25);
});
it('should cap estimated capacity at 100% when level exceeds maxLevel', () => {
const result = calculateLakeMetrics(115, 0, config);
expect(result.capacity).toBe(100);
expect(result.volume).toBe(50); // 100% of 50
});
it('should floor estimated capacity at 0% when level is below minLevel', () => {
const result = calculateLakeMetrics(90, 0, config);
expect(result.capacity).toBe(0);
expect(result.volume).toBe(0); // 0% of 50
});
it('should correctly calculate storageDiff', () => {
const result = calculateLakeMetrics(106, 25, config);
// 106 - 108 = -2.00
expect(result.storageDiff).toBe(-2);
});
it('should calculate positive storageDiff when above storageLevel', () => {
const result = calculateLakeMetrics(109, 25, config);
// 109 - 108 = 1.00
expect(result.storageDiff).toBe(1);
});
it('should handle missing config gracefully', () => {
const emptyConfig: LakeCalculationConfig = {};
const result = calculateLakeMetrics(105, 0, emptyConfig);
expect(result.capacity).toBe(0);
expect(result.volume).toBe(0);
expect(result.storageDiff).toBe(0);
});
});
+5 -19
View File
@@ -1,6 +1,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { lakesConfig } from './lakesConfig';
import { calculateLakeMetrics } from './utils/calculations';
interface DataRecord {
timestamp: string;
@@ -53,22 +54,7 @@ const lakes = lakesConfig.map(lake => {
}
}
if (volume > 0 && lake.maxVolume && lake.maxVolume > 0) {
capacity = Math.max(0, Math.min(100, Math.round((volume / lake.maxVolume) * 1000) / 10));
} else if (lake.minLevel && lake.maxLevel && currentLevel > 0) {
const percentage = ((currentLevel - lake.minLevel) / (lake.maxLevel - lake.minLevel)) * 100;
capacity = Math.max(0, Math.min(100, Math.round(percentage * 10) / 10)); // Round to 1 decimal place
if (volume === 0) {
volume = Number(((capacity / 100) * (lake.maxVolume || 0)).toFixed(1));
}
} else {
if (volume === 0) volume = lake.maxVolume || 0;
}
let storageDiff = 0;
if (lake.storageLevel && currentLevel > 0) {
storageDiff = Number((currentLevel - lake.storageLevel).toFixed(2));
}
const metrics = calculateLakeMetrics(currentLevel, volume, lake);
return {
id: lake.id,
@@ -76,11 +62,11 @@ const lakes = lakesConfig.map(lake => {
river: lake.text.includes('-') ? lake.text.split('-')[1].trim() : '',
priority: lake.priority || false,
level: currentLevel.toFixed(2),
capacity: capacity,
storageDiff: storageDiff,
capacity: metrics.capacity,
storageDiff: metrics.storageDiff,
inflow: inflow.toFixed(1),
outflow: currentFlow.toFixed(1),
volume: volume,
volume: metrics.volume,
maxVolume: lake.maxVolume || 0,
lat: lake.coords[0],
lng: lake.coords[1],
+52
View File
@@ -0,0 +1,52 @@
export interface LakeCalculationConfig {
maxVolume?: number;
minLevel?: number;
maxLevel?: number;
storageLevel?: number;
}
export interface LakeMetrics {
capacity: number; // 0-100 percentage
volume: number; // in mil. m3
storageDiff: number; // in meters
}
export function calculateLakeMetrics(
currentLevel: number,
reportedVolume: number,
config: LakeCalculationConfig
): LakeMetrics {
let capacity = 0;
let volume = reportedVolume;
let storageDiff = 0;
// 1. Calculate capacity and volume
if (volume > 0 && config.maxVolume && config.maxVolume > 0) {
// If real volume is available, calculate capacity from volume
capacity = Math.max(0, Math.min(100, Math.round((volume / config.maxVolume) * 1000) / 10));
} else if (config.minLevel && config.maxLevel && currentLevel > 0) {
// Fallback: estimate capacity and volume from level difference
const percentage = ((currentLevel - config.minLevel) / (config.maxLevel - config.minLevel)) * 100;
capacity = Math.max(0, Math.min(100, Math.round(percentage * 10) / 10)); // Round to 1 decimal place
if (volume === 0) {
volume = Number(((capacity / 100) * (config.maxVolume || 0)).toFixed(1));
}
} else {
// Missing required config data or bad level
if (volume === 0) {
volume = config.maxVolume || 0;
}
}
// 2. Calculate storage difference
if (config.storageLevel && currentLevel > 0) {
storageDiff = Number((currentLevel - config.storageLevel).toFixed(2));
}
return {
capacity,
volume,
storageDiff
};
}
+3 -1
View File
@@ -36,6 +36,8 @@ describe('KpiCards Component', () => {
const noDiffData = { ...mockData, storageDiff: 0, fullness: 85.5 };
render(<KpiCards data={noDiffData} language="cs" />);
expect(screen.getByText('85.5%')).toBeInTheDocument();
const elements = screen.getAllByText('85.5%');
expect(elements.length).toBeGreaterThan(0);
expect(elements[0]).toBeInTheDocument();
});
});