Skip to main content

qtty_core/units/
energy.rs

1// SPDX-License-Identifier: BSD-3-Clause
2// Copyright (C) 2026 Vallés Puig, Ramon
3
4//! Energy units.
5//!
6//! The canonical scaling unit for this dimension is the **joule** (`Joule::RATIO == 1.0`).
7//! All other energy units are expressed as exact ratios to joules.
8//!
9//! This module provides:
10//!
11//! - **SI joule** and commonly used SI prefixes.
12//! - **Erg** (feature: `fundamental-physics`) — CGS unit (1 erg = 10⁻⁷ J).
13//! - **Electronvolt** (feature: `fundamental-physics`) — 1 eV ≈ 1.602 176 634 × 10⁻¹⁹ J (exact, 2019 SI).
14//! - **Calorie / kilocalorie** (feature: `customary`) — thermochemical calorie.
15//!
16//! ```rust
17//! use qtty_core::energy::{Kilojoules, Joule};
18//!
19//! let kj = Kilojoules::new(1.0);
20//! let j = kj.to::<Joule>();
21//! assert_eq!(j.value(), 1000.0);
22//! ```
23
24use crate::{Quantity, Unit};
25use qtty_derive::Unit;
26
27/// Re-export the energy dimension from the dimension module.
28pub use crate::dimension::Energy;
29
30/// Marker trait for any [`Unit`] whose dimension is [`Energy`].
31pub trait EnergyUnit: Unit<Dim = Energy> {}
32impl<T: Unit<Dim = Energy>> EnergyUnit for T {}
33
34// ─────────────────────────────────────────────────────────────────────────────
35// SI joule
36// ─────────────────────────────────────────────────────────────────────────────
37
38/// Joule — SI coherent derived unit of energy (kg·m²/s²).
39#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
40#[unit(symbol = "J", dimension = Energy, ratio = 1.0)]
41pub struct Joule;
42/// A quantity measured in joules.
43pub type Joules = Quantity<Joule>;
44/// One joule.
45pub const JOULE: Joules = Joules::new(1.0);
46
47macro_rules! si_joule {
48    ($name:ident, $sym:literal, $ratio:expr, $qty:ident, $one:ident) => {
49        #[doc = concat!("SI-prefixed joule unit (", stringify!($ratio), " J).")]
50        #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
51        #[unit(symbol = $sym, dimension = Energy, ratio = $ratio)]
52        pub struct $name;
53        #[doc = concat!("A quantity measured in ", stringify!($name), "s.")]
54        pub type $qty = Quantity<$name>;
55        #[doc = concat!("One ", stringify!($name), ".")]
56        pub const $one: $qty = $qty::new(1.0);
57    };
58}
59
60si_joule!(Nanojoule, "nJ", 1e-9, Nanojoules, NANOJOULE);
61si_joule!(Picojoule, "pJ", 1e-12, Picojoules, PICOJOULE);
62si_joule!(Microjoule, "µJ", 1e-6, Microjoules, MICROJOULE);
63si_joule!(Millijoule, "mJ", 1e-3, Millijoules, MILLIJOULE);
64si_joule!(Kilojoule, "kJ", 1e3, Kilojoules, KILOJOULE);
65si_joule!(Megajoule, "MJ", 1e6, Megajoules, MEGAJOULE);
66si_joule!(Gigajoule, "GJ", 1e9, Gigajoules, GIGAJOULE);
67si_joule!(Terajoule, "TJ", 1e12, Terajoules, TERAJOULE);
68
69/// Watt-hour — 1 Wh = 3 600 J (exact).
70#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
71#[unit(symbol = "Wh", dimension = Energy, ratio = 3_600.0)]
72pub struct WattHour;
73/// A quantity measured in watt-hours.
74pub type WattHours = Quantity<WattHour>;
75/// One watt-hour.
76pub const WATT_HOUR: WattHours = WattHours::new(1.0);
77
78/// Kilowatt-hour — 1 kWh = 3 600 000 J (exact).
79#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
80#[unit(symbol = "kWh", dimension = Energy, ratio = 3_600_000.0)]
81pub struct KilowattHour;
82/// A quantity measured in kilowatt-hours.
83pub type KilowattHours = Quantity<KilowattHour>;
84/// One kilowatt-hour.
85pub const KILOWATT_HOUR: KilowattHours = KilowattHours::new(1.0);
86
87// ─────────────────────────────────────────────────────────────────────────────
88// Feature-gated units
89// ─────────────────────────────────────────────────────────────────────────────
90
91/// Erg — CGS unit of energy (1 erg = 10⁻⁷ J).
92#[cfg(feature = "fundamental-physics")]
93#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
94#[unit(symbol = "erg", dimension = Energy, ratio = 1e-7)]
95pub struct Erg;
96/// A quantity measured in ergs.
97#[cfg(feature = "fundamental-physics")]
98pub type Ergs = Quantity<Erg>;
99
100/// Electronvolt — 1 eV = 1.602 176 634 × 10⁻¹⁹ J (exact, 2019 SI redefinition).
101#[cfg(feature = "fundamental-physics")]
102#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
103#[unit(symbol = "eV", dimension = Energy, ratio = 1.602_176_634e-19)]
104pub struct Electronvolt;
105/// A quantity measured in electronvolts.
106#[cfg(feature = "fundamental-physics")]
107pub type Electronvolts = Quantity<Electronvolt>;
108
109/// Kilo-electronvolt (1 keV = 10³ eV).
110#[cfg(feature = "fundamental-physics")]
111#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
112#[unit(symbol = "keV", dimension = Energy, ratio = 1.602_176_634e-16)]
113pub struct Kiloelectronvolt;
114/// A quantity measured in kilo-electronvolts.
115#[cfg(feature = "fundamental-physics")]
116pub type Kiloelectronvolts = Quantity<Kiloelectronvolt>;
117
118/// Mega-electronvolt (1 MeV = 10⁶ eV).
119#[cfg(feature = "fundamental-physics")]
120#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
121#[unit(symbol = "MeV", dimension = Energy, ratio = 1.602_176_634e-13)]
122pub struct Megaelectronvolt;
123/// A quantity measured in mega-electronvolts.
124#[cfg(feature = "fundamental-physics")]
125pub type Megaelectronvolts = Quantity<Megaelectronvolt>;
126
127/// Thermochemical calorie (1 cal_th = 4.184 J, exact).
128#[cfg(feature = "customary")]
129#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
130#[unit(symbol = "cal", dimension = Energy, ratio = 4.184)]
131pub struct Calorie;
132/// A quantity measured in (thermochemical) calories.
133#[cfg(feature = "customary")]
134pub type Calories = Quantity<Calorie>;
135
136/// Kilocalorie (1 kcal = 4184 J, exact).
137#[cfg(feature = "customary")]
138#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
139#[unit(symbol = "kcal", dimension = Energy, ratio = 4184.0)]
140pub struct Kilocalorie;
141/// A quantity measured in kilocalories.
142#[cfg(feature = "customary")]
143pub type Kilocalories = Quantity<Kilocalorie>;
144
145/// British Thermal Unit — 1 BTU ≈ 1 055.05585262 J (ISO 31-4).
146#[cfg(feature = "customary")]
147#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
148#[unit(symbol = "BTU", dimension = Energy, ratio = 1_055.05585262)]
149pub struct BritishThermalUnit;
150/// A quantity measured in British thermal units.
151#[cfg(feature = "customary")]
152pub type BritishThermalUnits = Quantity<BritishThermalUnit>;
153
154/// Therm — 1 therm = 100 000 BTU = 105 505 585.262 J.
155#[cfg(feature = "customary")]
156#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
157#[unit(symbol = "therm", dimension = Energy, ratio = 105_505_585.262)]
158pub struct Therm;
159/// A quantity measured in therms.
160#[cfg(feature = "customary")]
161pub type Therms = Quantity<Therm>;
162
163// ─────────────────────────────────────────────────────────────────────────────
164// Unit inventory macro
165// ─────────────────────────────────────────────────────────────────────────────
166
167/// Canonical list of always-available (metric SI) energy units.
168#[macro_export]
169#[doc(hidden)]
170macro_rules! energy_units {
171    ($cb:path) => {
172        $cb!(
173            Joule,
174            Picojoule,
175            Nanojoule,
176            Microjoule,
177            Millijoule,
178            Kilojoule,
179            Megajoule,
180            Gigajoule,
181            Terajoule,
182            WattHour,
183            KilowattHour
184        );
185    };
186}
187
188// Generate bidirectional From impls between base metric SI energy units.
189energy_units!(crate::impl_unit_from_conversions);
190
191#[cfg(feature = "cross-unit-ops")]
192energy_units!(crate::impl_unit_cross_unit_ops);
193
194// ── Cross-feature: customary × fundamental-physics ───────────────────────────
195#[cfg(all(feature = "customary", feature = "fundamental-physics"))]
196crate::__impl_from_each_extra_to_bases!(
197    {Calorie, Kilocalorie, BritishThermalUnit, Therm}
198    Erg, Electronvolt, Kiloelectronvolt, Megaelectronvolt
199);
200#[cfg(all(
201    feature = "customary",
202    feature = "fundamental-physics",
203    feature = "cross-unit-ops"
204))]
205crate::__impl_cross_ops_each_extra_to_bases!(
206    {Calorie, Kilocalorie, BritishThermalUnit, Therm}
207    Erg, Electronvolt, Kiloelectronvolt, Megaelectronvolt
208);
209
210// Compile-time check: every base energy unit is registered as BuiltinUnit.
211#[cfg(test)]
212energy_units!(crate::assert_units_are_builtin);
213
214/// Canonical list of `fundamental-physics`-gated energy units (Erg, eV family).
215#[cfg(feature = "fundamental-physics")]
216#[macro_export]
217#[doc(hidden)]
218macro_rules! energy_fundamental_physics_units {
219    ($cb:path) => {
220        $cb!(Erg, Electronvolt, Kiloelectronvolt, Megaelectronvolt);
221    };
222}
223
224/// Canonical list of `customary`-gated energy units (calorie, kilocalorie, BTU, therm).
225#[cfg(feature = "customary")]
226#[macro_export]
227#[doc(hidden)]
228macro_rules! energy_customary_units {
229    ($cb:path) => {
230        $cb!(Calorie, Kilocalorie, BritishThermalUnit, Therm);
231    };
232}
233
234#[cfg(all(test, feature = "std"))]
235mod tests {
236    use super::*;
237    use approx::assert_abs_diff_eq;
238
239    #[test]
240    fn kilojoule_to_joule() {
241        let kj = Kilojoules::new(1.0);
242        let j: Joules = kj.to();
243        assert_abs_diff_eq!(j.value(), 1_000.0, epsilon = 1e-12);
244    }
245
246    #[test]
247    fn joule_to_millijoule() {
248        let j = Joules::new(1.0);
249        let mj: Millijoules = j.to();
250        assert_abs_diff_eq!(mj.value(), 1_000.0, epsilon = 1e-12);
251    }
252
253    #[test]
254    fn megajoule_to_kilojoule() {
255        let mj = Megajoules::new(1.0);
256        let kj: Kilojoules = mj.to();
257        assert_abs_diff_eq!(kj.value(), 1_000.0, epsilon = 1e-12);
258    }
259
260    #[test]
261    #[cfg(feature = "fundamental-physics")]
262    fn joule_to_erg() {
263        let j = Joules::new(1.0);
264        let e: Ergs = j.to();
265        assert_abs_diff_eq!(e.value(), 1e7, epsilon = 1e-5);
266    }
267
268    #[test]
269    #[cfg(feature = "fundamental-physics")]
270    fn ev_to_joule() {
271        let ev = Electronvolts::new(1.0);
272        let j: Joules = ev.to();
273        assert_abs_diff_eq!(j.value(), 1.602_176_634e-19, epsilon = 1e-30);
274    }
275
276    #[test]
277    #[cfg(feature = "customary")]
278    fn calorie_to_joule() {
279        let cal = Calories::new(1.0);
280        let j: Joules = cal.to();
281        assert_abs_diff_eq!(j.value(), 4.184, epsilon = 1e-12);
282    }
283
284    #[test]
285    #[cfg(feature = "customary")]
286    fn kilocalorie_to_joule() {
287        let kcal = Kilocalories::new(1.0);
288        let j: Joules = kcal.to();
289        assert_abs_diff_eq!(j.value(), 4184.0, epsilon = 1e-9);
290    }
291
292    #[test]
293    fn watt_hour_to_joule() {
294        let wh = WattHours::new(1.0);
295        let j: Joules = wh.to();
296        assert_abs_diff_eq!(j.value(), 3_600.0, epsilon = 1e-10);
297    }
298
299    #[test]
300    fn kilowatt_hour_to_joule() {
301        let kwh = KilowattHours::new(1.0);
302        let j: Joules = kwh.to();
303        assert_abs_diff_eq!(j.value(), 3_600_000.0, epsilon = 1e-6);
304    }
305
306    #[test]
307    fn nanojoule_to_picojoule() {
308        let nj = Nanojoules::new(1.0);
309        let pj: Picojoules = nj.to();
310        assert_abs_diff_eq!(pj.value(), 1_000.0, epsilon = 1e-9);
311    }
312
313    #[test]
314    #[cfg(feature = "customary")]
315    fn btu_to_joule() {
316        let btu = BritishThermalUnits::new(1.0);
317        let j: Joules = btu.to();
318        assert_abs_diff_eq!(j.value(), 1_055.05585262, epsilon = 1e-6);
319    }
320
321    #[test]
322    #[cfg(feature = "customary")]
323    fn therm_to_btu() {
324        let therm = Therms::new(1.0);
325        let btu: BritishThermalUnits = therm.to();
326        assert_abs_diff_eq!(btu.value(), 100_000.0, epsilon = 1e-3);
327    }
328}