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();
|
const payload = await response.json();
|
||||||
|
|
||||||
assert.equal(response.status, 200);
|
assert.equal(response.status, 200);
|
||||||
assert.equal(payload.catalogVersion, 5);
|
assert.equal(payload.catalogVersion, 6);
|
||||||
assert.deepEqual(payload.domains, ['mileage']);
|
assert.deepEqual(payload.domains, ['mileage']);
|
||||||
assert.ok(payload.metrics.some((metric: { id: string }) => metric.id === 'mileage.assessment_completion_rate'));
|
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();
|
const payload = await response.json();
|
||||||
|
|
||||||
assert.equal(response.status, 200);
|
assert.equal(response.status, 200);
|
||||||
assert.equal(payload.catalogVersion, 5);
|
assert.equal(payload.catalogVersion, 6);
|
||||||
assert.deepEqual(payload.domains, ['electric']);
|
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.charge_total_fee'));
|
||||||
assert.ok(payload.metrics.some((metric: { id: string }) => metric.id === 'electric.blended_cost_intensity'));
|
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();
|
const payload = await response.json();
|
||||||
|
|
||||||
assert.equal(response.status, 200);
|
assert.equal(response.status, 200);
|
||||||
assert.equal(payload.catalogVersion, 5);
|
assert.equal(payload.catalogVersion, 6);
|
||||||
assert.deepEqual(payload.domains, ['etc']);
|
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.total_amount'));
|
||||||
assert.ok(payload.metrics.some((metric: { id: string }) => metric.id === 'etc.bill_receivable'));
|
assert.ok(payload.metrics.some((metric: { id: string }) => metric.id === 'etc.bill_receivable'));
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
getManualMonitoringSnapshot,
|
getManualMonitoringSnapshot,
|
||||||
storeManualMonitoringSnapshot,
|
storeManualMonitoringSnapshot,
|
||||||
} from './manual-snapshot.js';
|
} from './manual-snapshot.js';
|
||||||
|
import { sumMileageKm } from './precision.js';
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
@@ -245,9 +246,9 @@ app.get('/', async (c) => {
|
|||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
totalToday: Math.round(filtered.reduce((sum, v) => sum + v.dailyKm, 0)),
|
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,
|
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) => {
|
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.ok(metrics.length >= 5);
|
||||||
assert.notEqual(metrics, METRIC_CATALOG);
|
assert.notEqual(metrics, METRIC_CATALOG);
|
||||||
assert.ok(metrics.every(metric => metric.domain === 'mileage'));
|
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', () => {
|
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';
|
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[] = [
|
const MILEAGE_METRICS: readonly MetricDefinition[] = [
|
||||||
{
|
{
|
||||||
@@ -72,6 +72,19 @@ const MILEAGE_METRICS: readonly MetricDefinition[] = [
|
|||||||
dimensions: ['assessment-year', 'assessment-target', 'department', 'customer', 'plate'],
|
dimensions: ['assessment-year', 'assessment-target', 'department', 'customer', 'plate'],
|
||||||
drillEntity: 'assessment-target',
|
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',
|
id: 'mileage.data_freshness',
|
||||||
domain: 'mileage',
|
domain: 'mileage',
|
||||||
|
|||||||
Reference in New Issue
Block a user