---
title: Measurement & Formatting
parent: Developer Guide
nav_order: 9
last_updated: 2026-05-13
aliases:
  - measurement
  - metric-formatter
  - number-formatter
---

# Measurement & Formatting

How the Meshtastic Android/KMP app formats numbers, units, and locale-sensitive values.

---

## Overview

All measurement data transmitted by Meshtastic radios uses **metric units** (meters, °C, hPa, m/s, etc.). The app converts and formats these values for display using two core utilities:

| Utility | Location | Purpose |
|---|---|---|
| `MetricFormatter` | `core/common/.../util/MetricFormatter.kt` | Converts and formats physical measurements (temperature, pressure, speed, etc.) |
| `NumberFormatter` | `core/common/.../util/NumberFormatter.kt` | Low-level fixed-point number formatting with locale-independent dot separator |

Both live in `org.meshtastic.core.common.util` and are available to all KMP targets (Android, Desktop, iOS).

---

## MetricFormatter API

`MetricFormatter` is a Kotlin `object` with pure functions for each measurement type:

```kotlin
object MetricFormatter {
    fun temperature(celsius: Float, isFahrenheit: Boolean): String
    fun voltage(volts: Float, decimalPlaces: Int = 2): String
    fun current(milliAmps: Float, decimalPlaces: Int = 1): String
    fun percent(value: Float, decimalPlaces: Int = 1): String
    fun humidity(value: Float): String
    fun pressure(hPa: Float, decimalPlaces: Int = 1): String
    fun snr(value: Float, decimalPlaces: Int = 1): String
    fun rssi(value: Int): String
    fun windSpeed(metersPerSecond: Float, decimalPlaces: Int = 1): String
    fun rainfall(millimeters: Float, decimalPlaces: Int = 1): String
}
```

### Usage

```kotlin
// Temperature — Fahrenheit conversion is handled automatically
MetricFormatter.temperature(22.5f, isFahrenheit = true)  // "72.5°F"
MetricFormatter.temperature(22.5f, isFahrenheit = false)  // "22.5°C"

// Signal metrics
MetricFormatter.snr(-5.2f)    // "-5.2 dB"
MetricFormatter.rssi(-97)     // "-97 dBm"

// Environment
MetricFormatter.pressure(1013.25f)  // "1013.3 hPa"
MetricFormatter.humidity(65.0f)     // "65%"
MetricFormatter.windSpeed(3.7f)     // "3.7 m/s"
MetricFormatter.rainfall(12.3f)     // "12.3 mm"

// Power
MetricFormatter.voltage(3.95f)      // "3.95 V"
MetricFormatter.current(125.0f)     // "125.0 mA"
```

---

## NumberFormatter

`NumberFormatter` provides locale-independent decimal formatting using pure arithmetic (no `String.format` or `DecimalFormat`):

```kotlin
object NumberFormatter {
    fun format(value: Double, decimalPlaces: Int): String
    fun format(value: Float, decimalPlaces: Int): String
}
```

> **Why locale-independent?** Meshtastic is a mesh networking app where consistency matters — sensor readings shared between nodes should look the same everywhere. `NumberFormatter` always uses `.` as the decimal separator.

---

## Temperature Conversion

Temperature is the only measurement that performs a unit conversion. The `isFahrenheit` flag is typically sourced from the user's device locale or preferences:

```
°F = °C × 1.8 + 32
```

All other measurements display in their native metric units. The user-facing `units-and-locale.md` page explains what end users see.

---

## Adding a New Measurement Type

To add a new measurement formatter:

1. **Add a function to `MetricFormatter`** in `core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/MetricFormatter.kt`:

   ```kotlin
   fun radiation(microSieverts: Float, decimalPlaces: Int = 2): String =
       "${NumberFormatter.format(microSieverts, decimalPlaces)} μSv/h"
   ```

2. **Add tests** in `core/common/src/commonTest/`:

   ```kotlin
   @Test
   fun radiationFormatting() {
       assertEquals("0.15 μSv/h", MetricFormatter.radiation(0.15f))
       assertEquals("1.23 μSv/h", MetricFormatter.radiation(1.234f))
   }
   ```

3. **Use in UI** — call from any `commonMain` composable or ViewModel:

   ```kotlin
   Text(text = MetricFormatter.radiation(node.radiationLevel))
   ```

4. **Run verification**:
   ```bash
   ./gradlew :core:common:allTests
   ```

---

## DateFormatter

Date and time formatting uses the `DateFormatter` interface with platform-specific implementations:

| Function | Output Example |
|---|---|
| `formatRelativeTime()` | "5 min ago" |
| `formatDateTime()` | "May 13, 2026 2:30 PM" |
| `formatShortDate()` | "May 13" |
| `formatTime()` | "2:30 PM" |
| `formatTimeWithSeconds()` | "2:30:45 PM" |
| `formatDate()` | "2026-05-13" |

Unlike `MetricFormatter`, `DateFormatter` is an **interface** with platform `expect`/`actual` implementations because date formatting inherently depends on platform locale APIs.

---

## Design Decisions

| Decision | Rationale |
|---|---|
| Locale-independent decimal separator (`.`) | Mesh data shared between nodes must be consistent |
| Pure arithmetic formatting (no `DecimalFormat`) | Works identically on JVM, Native, and JS targets |
| Temperature is the only converted unit | All other metric units are universally understood in their native form |
| `object` singleton pattern | Stateless utility — no instance management needed |

---

## Related

- **User-facing docs**: `docs/user/units-and-locale.md` explains what end users see
- **Source code**: `core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/MetricFormatter.kt`
- **Tests**: `core/common/src/commonTest/kotlin/org/meshtastic/core/common/util/MetricFormatterTest.kt`
