// Shared numeric helpers: longitudes, latitudes and fixed-point formatting.
// Everything here is total: no input can make these functions panic, and
// non-finite inputs are rejected by the callers that need a value.
///|
/// Degrees to radians.
pub fn to_radians(degrees : Double) -> Double {
degrees * 0.017453292519943295
}
///|
/// Normalise a longitude into `[-180, 180)`. Non-finite values are returned
/// unchanged so callers can test them.
pub fn normalize_lng(lng : Double) -> Double {
if lng.is_nan() || lng.is_inf() {
return lng
}
let mut x = lng % 360.0
if x < -180.0 {
x = x + 360.0
}
if x >= 180.0 {
x = x - 360.0
}
x
}
///|
/// The signed shortest difference `b - a` between two longitudes, in degrees.
pub fn lng_delta(a : Double, b : Double) -> Double {
normalize_lng(b - a)
}
///|
/// Clamp a latitude into `[-90, 90]`. Non-finite values are returned unchanged.
pub fn clamp_lat(lat : Double) -> Double {
if lat.is_nan() {
lat
} else if lat < -90.0 {
-90.0
} else if lat > 90.0 {
90.0
} else {
lat
}
}
///|
/// True when a double is finite (not NaN, not infinite).
pub fn is_finite(x : Double) -> Bool {
!(x.is_nan() || x.is_inf())
}
///|
/// Lossless Double text without an exponent, for RFC 5870 and JSON consumers.
/// Fixed decimal rounding must not turn a tiny positive uncertainty into zero.
pub fn decimal_notation(value : Double) -> String {
let text = ascii_lower(value.to_string())
let parts = split_text(text, 'e')
if parts.length() != 2 {
return text
}
let exponent = parse_decimal(parts[1]).unwrap().to_int()
let negative = parts[0][0] == '-'
let mantissa = if negative { parts[0][1:].to_owned() } else { parts[0] }
let digits = split_text(mantissa, '.')
let tail = if digits.length() == 2 { digits[1] } else { "" }
let all = digits[0] + tail
let place = digits[0].length() + exponent
let expanded = if place <= 0 {
"0." + "0".repeat(-place) + all
} else if place >= all.length() {
all + "0".repeat(place - all.length())
} else {
all[0:place].to_owned() + "." + all[place:].to_owned()
}
if negative {
"-" + expanded
} else {
expanded
}
}
///|
/// Round `x` to `digits` decimal places, half away from zero.
pub fn round_to(x : Double, digits : Int32) -> Double {
let scale = pow10(digits)
if x >= 0.0 {
(x * scale + 0.5).floor() / scale
} else {
(x * scale - 0.5).ceil() / scale
}
}
///|
/// `10.0 ^ n` for a small non-negative `n`.
fn pow10(n : Int32) -> Double {
let mut p = 1.0
for _ in 0.. Int64 {
let mut p = 1L
for _ in 0.. String {
if x.is_nan() {
return "nan"
}
if x.is_inf() {
if x > 0.0 {
return "inf"
}
return "-inf"
}
let d = if digits < 0 { 0 } else if digits > 9 { 9 } else { digits }
let scale = pow10_i64(d).to_double()
let scaled = x * scale
let rounded : Int64 = if scaled >= 0.0 {
(scaled + 0.5).floor().to_int64()
} else {
(scaled - 0.5).ceil().to_int64()
}
let negative = rounded < 0L
let magnitude = if negative { -rounded } else { rounded }
let raw = magnitude.to_string()
let padded = if d == 0 {
raw
} else if raw.length() <= d {
"0".repeat(d - raw.length() + 1) + raw
} else {
raw
}
let body = if d == 0 {
padded
} else {
let cut = padded.length() - d
padded[0:cut].to_owned() + "." + padded[cut:].to_owned()
}
if negative && magnitude != 0L {
"-" + body
} else {
body
}
}
///|
/// Format a latitude/longitude pair as `lat, lng`.
pub fn format_latlng(lat : Double, lng : Double, digits : Int32) -> String {
format_fixed(lat, digits) + ", " + format_fixed(lng, digits)
}
///|
/// A degree symbol, quoted so the source stays ASCII-only.
pub const DEGREE : String = "\u{00B0}"
///|
/// A prime (minutes) symbol.
pub const PRIME : String = "\u{2032}"
///|
/// A double prime (seconds) symbol.
pub const DOUBLE_PRIME : String = "\u{2033}"
///|
/// Two-letter hemisphere suffix for a coordinate value.
pub fn hemisphere_suffix(value : Double, is_lat : Bool) -> String {
if is_lat {
if value < 0.0 {
"S"
} else {
"N"
}
} else if value < 0.0 {
"W"
} else {
"E"
}
}