Files
rippr/lib/src/ui/format.dart
Dylan 4af4e3411a Port Telemetry, Format, RideStatistics; add cross-language parity harness
T03 — telemetry.dart, format.dart, live_telemetry.dart, plus pure domain models
(Trip/Segment/TrackPoint/RideStats) with no persistence dependency, so Drift can
map to them in T08 rather than the domain depending on the database.

T04 — ride_statistics.dart including ElevationAccumulator, ported structurally
faithfully: moving average, reversal hysteresis, gainIncludingPending, and the
finish() reconciliation against lastRaw.

T07 (early, because T04 forced it) — tool/parity/ drives identical fixtures
through the real Kotlin files and the Dart port, then diffs. Result: every value
byte-identical, including noisy_gain=38.959594555022136 to the last digit. The
sole difference is run_avg_speed, where Kotlin's 32-bit Float widens to double
with artefacts Dart's binary64 does not reproduce. Documented, not papered over.

That harness settled a real question. The ported elevation test failed at 50.9m
against Kotlin's 35m bound, which looked like a porting bug. It was not: Kotlin's
and Dart's Random(42) are different streams. On a shared LCG fixture both produce
39.0m -- which would also fail Kotlin's own bound. The native guard passes on seed
luck rather than on a property of the algorithm. The Dart test now uses the shared
LCG, asserts bit-equality with Kotlin, and sets its bound from measured behaviour
(25 seeds spanned 24.7-46.7m).

52 tests passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 20:59:35 -05:00

36 lines
1.3 KiB
Dart

/// Display formatting shared across screens.
///
/// Ported from `com.rippr.ui.Format`.
///
/// Timestamps come from the platform location fix, which is UTC epoch millis, so
/// everything here converts to the device zone. Formatting in UTC would show a 21:00
/// ride as tomorrow.
library;
import 'package:intl/intl.dart';
import '../domain/models.dart';
// Built per call rather than cached in a top-level final. The Kotlin original captured
// Locale.getDefault() once at class-init; doing the same here would freeze the format
// for the process lifetime and ignore a locale change.
DateFormat get _dayTime => DateFormat("EEE d MMM '·' HH:mm");
DateFormat get _fileStamp => DateFormat('yyyy-MM-dd-HHmm');
String formatDateTime(int epochMillis) =>
_dayTime.format(DateTime.fromMillisecondsSinceEpoch(epochMillis));
String formatFileTimestamp(int epochMillis) =>
_fileStamp.format(DateTime.fromMillisecondsSinceEpoch(epochMillis));
/// A trip's own name, or a date-derived label when it has none.
String tripLabel(Trip trip) => trip.name ?? formatDateTime(trip.startedAt);
String formatDistance(double meters) => meters < 1000
? '${meters.toInt()} m'
: '${(meters / 1000).toStringAsFixed(1)} km';
String formatSpeed(double kmh) => '${kmh.toStringAsFixed(1)} km/h';
String formatElevation(double meters) => '${meters.toInt()} m';