refactor: stabilize cumulative mileage metrics
This commit is contained in:
@@ -7,7 +7,7 @@ test('returns the published mileage metric contract', async () => {
|
||||
const payload = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(payload.catalogVersion, 5);
|
||||
assert.equal(payload.catalogVersion, 6);
|
||||
assert.deepEqual(payload.domains, ['mileage']);
|
||||
assert.ok(payload.metrics.some((metric: { id: string }) => metric.id === 'mileage.assessment_completion_rate'));
|
||||
});
|
||||
@@ -27,7 +27,7 @@ test('returns the published electric metric contract', async () => {
|
||||
const payload = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(payload.catalogVersion, 5);
|
||||
assert.equal(payload.catalogVersion, 6);
|
||||
assert.deepEqual(payload.domains, ['electric']);
|
||||
assert.ok(payload.metrics.some((metric: { id: string }) => metric.id === 'electric.charge_total_fee'));
|
||||
assert.ok(payload.metrics.some((metric: { id: string }) => metric.id === 'electric.blended_cost_intensity'));
|
||||
@@ -39,7 +39,7 @@ test('returns the published ETC metric contract', async () => {
|
||||
const payload = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(payload.catalogVersion, 5);
|
||||
assert.equal(payload.catalogVersion, 6);
|
||||
assert.deepEqual(payload.domains, ['etc']);
|
||||
assert.ok(payload.metrics.some((metric: { id: string }) => metric.id === 'etc.total_amount'));
|
||||
assert.ok(payload.metrics.some((metric: { id: string }) => metric.id === 'etc.bill_receivable'));
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getManualMonitoringSnapshot,
|
||||
storeManualMonitoringSnapshot,
|
||||
} from './manual-snapshot.js';
|
||||
import { sumMileageKm } from './precision.js';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
@@ -245,9 +246,9 @@ app.get('/', async (c) => {
|
||||
|
||||
const stats = {
|
||||
totalToday: Math.round(filtered.reduce((sum, v) => sum + v.dailyKm, 0)),
|
||||
totalAll: filtered.reduce((sum, v) => sum + (v.totalKm || 0), 0),
|
||||
totalAll: sumMileageKm(filtered.map(v => v.totalKm || 0)),
|
||||
vehicleCount: filtered.length,
|
||||
yesterdayTotal: filtered.reduce((sum, v) => sum + v.yesterdayKm, 0),
|
||||
yesterdayTotal: sumMileageKm(filtered.map(v => v.yesterdayKm)),
|
||||
};
|
||||
|
||||
const sorted = [...filtered].sort((a, b) => {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { roundMileageKm, sumMileageKm } from './precision.js';
|
||||
|
||||
test('removes binary floating point tails at the published precision', () => {
|
||||
assert.equal(sumMileageKm([0.1, 0.2]), 0.3);
|
||||
assert.equal(roundMileageKm(44_488_200.300000004), 44_488_200.3);
|
||||
});
|
||||
|
||||
test('rounds after aggregation and ignores invalid values', () => {
|
||||
assert.equal(sumMileageKm([0.04, 0.04]), 0.1);
|
||||
assert.equal(sumMileageKm([1, Number.NaN, Number.POSITIVE_INFINITY]), 1);
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
export function roundMileageKm(value: number, decimals = 1): number {
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
const scale = 10 ** decimals;
|
||||
return Math.round((value + Number.EPSILON) * scale) / scale;
|
||||
}
|
||||
|
||||
export function sumMileageKm(values: Iterable<number>, decimals = 1): number {
|
||||
let total = 0;
|
||||
for (const value of values) {
|
||||
if (Number.isFinite(value)) total += value;
|
||||
}
|
||||
return roundMileageKm(total, decimals);
|
||||
}
|
||||
@@ -19,6 +19,10 @@ test('lists mileage metrics without exposing the catalog array', () => {
|
||||
assert.ok(metrics.length >= 5);
|
||||
assert.notEqual(metrics, METRIC_CATALOG);
|
||||
assert.ok(metrics.every(metric => metric.domain === 'mileage'));
|
||||
assert.equal(
|
||||
metrics.find(metric => metric.id === 'mileage.cumulative_odometer_sum')?.timeSemantics,
|
||||
'snapshot',
|
||||
);
|
||||
});
|
||||
|
||||
test('publishes hydrogen cost, revenue, and gross profit as separate metrics', () => {
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface MetricDefinition {
|
||||
drillEntity: 'vehicle' | 'assessment-target' | 'hydrogen-station' | 'hydrogen-customer' | 'hydrogen-order' | 'electric-charge-order' | 'etc-toll-record' | 'etc-bill';
|
||||
}
|
||||
|
||||
export const METRIC_CATALOG_VERSION = 5;
|
||||
export const METRIC_CATALOG_VERSION = 6;
|
||||
|
||||
const MILEAGE_METRICS: readonly MetricDefinition[] = [
|
||||
{
|
||||
@@ -72,6 +72,19 @@ const MILEAGE_METRICS: readonly MetricDefinition[] = [
|
||||
dimensions: ['assessment-year', 'assessment-target', 'department', 'customer', 'plate'],
|
||||
drillEntity: 'assessment-target',
|
||||
},
|
||||
{
|
||||
id: 'mileage.cumulative_odometer_sum',
|
||||
domain: 'mileage',
|
||||
label: '累计里程合计',
|
||||
description: '当前筛选范围内每辆车最新累计里程表值之和,是快照指标,不能跨日期累加。',
|
||||
unit: 'km',
|
||||
aggregation: 'sum',
|
||||
timeSemantics: 'snapshot',
|
||||
formula: 'ROUND(SUM(latest_vehicle_total_mileage_km), 1)',
|
||||
sources: ['OneOS mileage API'],
|
||||
dimensions: ['department', 'region', 'customer', 'vehicle-model', 'plate'],
|
||||
drillEntity: 'vehicle',
|
||||
},
|
||||
{
|
||||
id: 'mileage.data_freshness',
|
||||
domain: 'mileage',
|
||||
|
||||
Reference in New Issue
Block a user