naira_format/
lib.rs

1// Naira Format API
2/// Format a number into Nigerian Naira format.
3///
4/// Examples:
5
6#[derive(Debug, PartialEq)]
7pub enum ParseError {
8    EmptyInput,
9    InvalidFormat,
10}
11
12pub fn format_naira<T>(amount: T) -> String
13where
14    T: Into<f64>,
15{
16    let value = amount.into();
17    let sign = if value < 0.0 { "-" } else { "" };
18    let absolute_value = value.abs();
19
20    // splitting the whole and fractional parts
21
22    let whole = absolute_value.trunc() as i64;
23    let decimal_part = ((absolute_value - absolute_value.trunc()) * 100.0).round() as u32;
24
25    let formatted_whole = format_with_commas(whole);
26
27    format!("₦{}{}.{:02}", sign, formatted_whole, decimal_part)
28}
29
30pub fn format_kobo_to_naira<T>(amount: T) -> String
31where
32    T: Into<f64>,
33{
34    let value = amount.into();
35    let sign = if value < 0.0 { "-" } else { "" };
36    let abs_kobo = value.abs().round() as i64;
37
38    // Convert kobo to naira + decimal
39    let whole_naira = abs_kobo / 100;
40    let decimal_part = abs_kobo % 100;
41
42    // Format whole naira with commas
43    let formatted_whole = format_with_commas(whole_naira);
44
45    format!("₦{}{}.{:02}", sign, formatted_whole, decimal_part)
46}
47
48pub fn format_naira_to_kobo<T>(amount: T) -> String
49where
50    T: Into<f64>,
51{
52    let value = amount.into();
53    let sign = if value < 0.0 { "-" } else { "" };
54    let kobo_amount = (value.abs() * 100.0).round() as i64;
55    format!("{}{}", sign, kobo_amount)
56}
57
58pub fn format_naira_compact<T>(amount: T) -> String
59where
60    T: Into<f64>,
61{
62    let value = amount.into();
63    let sign = if value < 0.0 { "-" } else { "" };
64    let absolute_value = value.abs();
65    if absolute_value < 1_000.0 {
66        // Less than 1K: show full number
67        let whole = absolute_value.trunc() as i64;
68        let decimal_part = ((absolute_value - absolute_value.trunc()) * 100.0).round() as u32;
69        if decimal_part == 0 {
70            format!("₦{}{}", sign, whole)
71        } else {
72            format!("₦{}{}.{:02}", sign, whole, decimal_part)
73        }
74    } else if absolute_value < 1_000_000.0 {
75        // Thousands: show with K suffix
76        let thousands = absolute_value / 1_000.0;
77        let whole_k = thousands.trunc() as i64;
78        let decimal_k = ((thousands - thousands.trunc()) * 10.0).round() as u32;
79        if decimal_k == 0 {
80            format!("₦{}{}K", sign, whole_k)
81        } else {
82            format!("₦{}{}.{}K", sign, whole_k, decimal_k)
83        }
84    } else if absolute_value < 1_000_000_000.0 {
85        // Millions: show with M suffix
86        let millions = absolute_value / 1_000_000.0;
87        let whole_m = millions.trunc() as i64;
88        let decimal_m = ((millions - millions.trunc()) * 10.0).round() as u32;
89        if decimal_m == 0 {
90            format!("₦{}{}M", sign, whole_m)
91        } else {
92            format!("₦{}{}.{}M", sign, whole_m, decimal_m)
93        }
94    } else {
95        // Billions: show with B suffix
96        let billions = absolute_value / 1_000_000_000.0;
97        let whole_b = billions.trunc() as i64;
98        let decimal_b = ((billions - billions.trunc()) * 10.0).round() as u32;
99        if decimal_b == 0 {
100            format!("₦{}{}B", sign, whole_b)
101        } else {
102            format!("₦{}{}.{}B", sign, whole_b, decimal_b)
103        }
104    }
105}
106
107pub fn parse_naira(input: &str) -> Result<f64, ParseError> {
108    let trimmed = input.trim();
109
110    if trimmed.is_empty() {
111        return Err(ParseError::EmptyInput);
112    }
113
114    // Detect sign
115    let (sign, value) = if let Some(stripped) = trimmed.strip_prefix('-') {
116        (-1.0, stripped)
117    } else {
118        (1.0, trimmed)
119    };
120
121    // Remove Naira symbol and commas
122    let cleaned = value.replace(['₦', ','], "");
123
124    // Parse to f64
125    let number: f64 = cleaned.parse().map_err(|_| ParseError::InvalidFormat)?;
126
127    Ok(sign * number)
128}
129
130fn format_with_commas(n: i64) -> String {
131    let s = n.to_string();
132    let sbyte = s.as_bytes();
133    let mut result = String::new();
134
135    let len = sbyte.len();
136    for (i, &b) in sbyte.iter().enumerate() {
137        if i > 0 && (len - i) % 3 == 0 {
138            result.push(',');
139        }
140        result.push(b as char);
141    }
142
143    result
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_format_naira() {
152        assert_eq!(format_naira(1234567.89), "₦1,234,567.89");
153        assert_eq!(format_naira(-55000), "₦-55,000.00");
154    }
155
156    #[test]
157    fn test_commas() {
158        assert_eq!(format_with_commas(1), "1");
159        assert_eq!(format_with_commas(1234567), "1,234,567");
160        assert_eq!(format_with_commas(-55000), "-55,000");
161    }
162
163    #[test]
164    fn test_format_kobo_to_naira() {
165        assert_eq!(format_kobo_to_naira(1), "₦0.01");
166        assert_eq!(format_kobo_to_naira(50), "₦0.50");
167        assert_eq!(format_kobo_to_naira(100), "₦1.00");
168        assert_eq!(format_kobo_to_naira(150000), "₦1,500.00");
169        assert_eq!(format_kobo_to_naira(-7550), "₦-75.50");
170    }
171
172    #[test]
173    fn test_format_naira_to_kobo() {
174        assert_eq!(format_naira_to_kobo(0.01), "1");
175        assert_eq!(format_naira_to_kobo(1234567.89), "123456789");
176        assert_eq!(format_naira_to_kobo(-75.50), "-7550");
177    }
178
179    #[test]
180    fn test_format_naira_compact() {
181        assert_eq!(format_naira_compact(950), "₦950");
182        assert_eq!(format_naira_compact(1200), "₦1.2K");
183        assert_eq!(format_naira_compact(2500000), "₦2.5M");
184    }
185    #[test]
186    fn test_parse_naira() {
187        assert_eq!(parse_naira("₦1,500.50"), Ok(1500.5));
188        assert_eq!(parse_naira("1500"), Ok(1500.0));
189        assert_eq!(parse_naira("1,234"), Ok(1234.0));
190        assert_eq!(parse_naira("-₦75.50"), Ok(-75.5));
191        assert_eq!(parse_naira(""), Err(ParseError::EmptyInput));
192        assert_eq!(parse_naira("abc"), Err(ParseError::InvalidFormat));
193    }
194}