Skip to main content

mathypad_core/units/
parser.rs

1//! Unit parsing functionality
2
3use super::types::Unit;
4use crate::UnitType;
5use crate::rate_unit;
6
7/// Parse a unit string into a Unit enum variant
8pub fn parse_unit(text: &str) -> Option<Unit> {
9    // First try case-sensitive matching for bits vs bytes disambiguation
10    match text {
11        // Bit units (lowercase 'b' for bits)
12        "bit" | "bits" => return Some(Unit::Bit),
13        "Kb" => return Some(Unit::Kb),
14        "Mb" => return Some(Unit::Mb),
15        "Gb" => return Some(Unit::Gb),
16        "Tb" => return Some(Unit::Tb),
17        "Pb" => return Some(Unit::Pb),
18        "Eb" => return Some(Unit::Eb),
19        "Kib" => return Some(Unit::Kib),
20        "Mib" => return Some(Unit::Mib),
21        "Gib" => return Some(Unit::Gib),
22        "Tib" => return Some(Unit::Tib),
23        "Pib" => return Some(Unit::Pib),
24        "Eib" => return Some(Unit::Eib),
25
26        // Byte units (uppercase 'B' for bytes)
27        "B" | "byte" | "bytes" => return Some(Unit::Byte),
28        "KB" => return Some(Unit::KB),
29        "MB" => return Some(Unit::MB),
30        "GB" => return Some(Unit::GB),
31        "TB" => return Some(Unit::TB),
32        "PB" => return Some(Unit::PB),
33        "EB" => return Some(Unit::EB),
34        "KiB" => return Some(Unit::KiB),
35        "MiB" => return Some(Unit::MiB),
36        "GiB" => return Some(Unit::GiB),
37        "TiB" => return Some(Unit::TiB),
38        "PiB" => return Some(Unit::PiB),
39        "EiB" => return Some(Unit::EiB),
40
41        // Traditional rate unit patterns - create generic rates
42        "bps" | "bit/s" | "bits/s" => {
43            return Some(rate_unit!(Unit::Bit, Unit::Second));
44        }
45        "Kbps" | "Kb/s" => return Some(rate_unit!(Unit::Kb, Unit::Second)),
46        "Mbps" | "Mb/s" => return Some(rate_unit!(Unit::Mb, Unit::Second)),
47        "Gbps" | "Gb/s" => return Some(rate_unit!(Unit::Gb, Unit::Second)),
48        "Tbps" | "Tb/s" => return Some(rate_unit!(Unit::Tb, Unit::Second)),
49        "Pbps" | "Pb/s" => return Some(rate_unit!(Unit::Pb, Unit::Second)),
50        "Ebps" | "Eb/s" => return Some(rate_unit!(Unit::Eb, Unit::Second)),
51        "Kibps" | "Kib/s" => {
52            return Some(rate_unit!(Unit::Kib, Unit::Second));
53        }
54        "Mibps" | "Mib/s" => {
55            return Some(rate_unit!(Unit::Mib, Unit::Second));
56        }
57        "Gibps" | "Gib/s" => {
58            return Some(rate_unit!(Unit::Gib, Unit::Second));
59        }
60        "Tibps" | "Tib/s" => {
61            return Some(rate_unit!(Unit::Tib, Unit::Second));
62        }
63        "Pibps" | "Pib/s" => {
64            return Some(rate_unit!(Unit::Pib, Unit::Second));
65        }
66        "Eibps" | "Eib/s" => {
67            return Some(rate_unit!(Unit::Eib, Unit::Second));
68        }
69
70        // Byte rates (uppercase 'B/s' for bytes per second)
71        "B/s" => return Some(rate_unit!(Unit::Byte, Unit::Second)),
72        "KB/s" => return Some(rate_unit!(Unit::KB, Unit::Second)),
73        "MB/s" => return Some(rate_unit!(Unit::MB, Unit::Second)),
74        "GB/s" => return Some(rate_unit!(Unit::GB, Unit::Second)),
75        "TB/s" => return Some(rate_unit!(Unit::TB, Unit::Second)),
76        "PB/s" => return Some(rate_unit!(Unit::PB, Unit::Second)),
77        "EB/s" => return Some(rate_unit!(Unit::EB, Unit::Second)),
78        "KiB/s" => return Some(rate_unit!(Unit::KiB, Unit::Second)),
79        "MiB/s" => return Some(rate_unit!(Unit::MiB, Unit::Second)),
80        "GiB/s" => return Some(rate_unit!(Unit::GiB, Unit::Second)),
81        "TiB/s" => return Some(rate_unit!(Unit::TiB, Unit::Second)),
82        "PiB/s" => return Some(rate_unit!(Unit::PiB, Unit::Second)),
83        "EiB/s" => return Some(rate_unit!(Unit::EiB, Unit::Second)),
84
85        _ => {} // Fall through to case-insensitive matching
86    }
87
88    // Case-insensitive matching for remaining units
89    match text.to_lowercase().as_str() {
90        "ns" | "nanosec" | "nanosecond" | "nanoseconds" => Some(Unit::Nanosecond),
91        "us" | "µs" | "microsec" | "microsecond" | "microseconds" => Some(Unit::Microsecond),
92        "ms" | "millisec" | "millisecond" | "milliseconds" => Some(Unit::Millisecond),
93        "s" | "sec" | "second" | "seconds" => Some(Unit::Second),
94        "min" | "minute" | "minutes" => Some(Unit::Minute),
95        "h" | "hr" | "hour" | "hours" => Some(Unit::Hour),
96        "day" | "days" => Some(Unit::Day),
97        "week" | "weeks" | "wk" | "wks" => Some(Unit::Week),
98        "month" | "months" | "mo" | "mos" => Some(Unit::Month),
99        "quarter" | "quarters" | "qtr" | "qtrs" => Some(Unit::Quarter),
100        "year" | "years" | "yr" | "yrs" => Some(Unit::Year),
101
102        // Case-insensitive parsing (backwards compatibility)
103        // For ambiguous lowercase units, follow networking conventions:
104        // - Byte units (kb, mb, gb) default to bytes
105        // - Bit units (kib, mib, gib when lowercase) default to base 10 bits for simplicity
106        "b" | "byte" | "bytes" => Some(Unit::Byte),
107        "kb" => Some(Unit::KB), // Kilobytes
108        "mb" => Some(Unit::MB), // Megabytes
109        "gb" => Some(Unit::GB), // Gigabytes
110        "tb" => Some(Unit::TB),
111        "pb" => Some(Unit::PB),
112        "eb" => Some(Unit::EB),
113
114        // For lowercase "ib" units - network-relevant sizes map to base 10 bits
115        // Large units that are rarely used in networking keep traditional binary interpretation
116        "kib" => Some(Unit::Kb), // Kilobits (base 10) - commonly used in networking
117        "mib" => Some(Unit::Mb), // Megabits (base 10) - commonly used in networking
118        "gib" => Some(Unit::Gb), // Gigabits (base 10) - commonly used in networking
119        "tib" => Some(Unit::TiB), // Keep as Tebibytes - rarely used in networking
120        "pib" => Some(Unit::PiB), // Keep as Pebibytes - rarely used in networking
121        "eib" => Some(Unit::EiB), // Keep as Exbibytes - rarely used in networking
122
123        // Case-insensitive rate parsing - create generic rates
124        // For "bps" suffix: bits take precedence (network convention)
125        // For "/s" suffix: bytes take precedence (file transfer convention)
126        "b/s" | "bytes/s" => Some(rate_unit!(Unit::Byte, Unit::Second)),
127        "kb/s" => Some(rate_unit!(Unit::KB, Unit::Second)),
128        "mb/s" => Some(rate_unit!(Unit::MB, Unit::Second)),
129        "gb/s" => Some(rate_unit!(Unit::GB, Unit::Second)),
130        "tb/s" => Some(rate_unit!(Unit::TB, Unit::Second)),
131        "pb/s" => Some(rate_unit!(Unit::PB, Unit::Second)),
132        "eb/s" => Some(rate_unit!(Unit::EB, Unit::Second)),
133        "kib/s" => Some(rate_unit!(Unit::KiB, Unit::Second)),
134        "mib/s" => Some(rate_unit!(Unit::MiB, Unit::Second)),
135        "gib/s" => Some(rate_unit!(Unit::GiB, Unit::Second)),
136        "tib/s" => Some(rate_unit!(Unit::TiB, Unit::Second)),
137        "pib/s" => Some(rate_unit!(Unit::PiB, Unit::Second)),
138        "eib/s" => Some(rate_unit!(Unit::EiB, Unit::Second)),
139
140        // For "bps" suffix: default to bits (network convention)
141        // Exception: very large units (PB/EB) default to bytes for backwards compatibility
142        "bps" => Some(rate_unit!(Unit::Bit, Unit::Second)),
143        "kbps" => Some(rate_unit!(Unit::Kb, Unit::Second)),
144        "mbps" => Some(rate_unit!(Unit::Mb, Unit::Second)),
145        "gbps" => Some(rate_unit!(Unit::Gb, Unit::Second)),
146        "tbps" => Some(rate_unit!(Unit::Tb, Unit::Second)),
147        "pbps" => Some(rate_unit!(Unit::PB, Unit::Second)), // Exception: PB default to bytes
148        "ebps" => Some(rate_unit!(Unit::EB, Unit::Second)), // Exception: EB default to bytes
149        "kibps" => Some(rate_unit!(Unit::Kib, Unit::Second)),
150        "mibps" => Some(rate_unit!(Unit::Mib, Unit::Second)),
151        "gibps" => Some(rate_unit!(Unit::Gib, Unit::Second)),
152        "tibps" => Some(rate_unit!(Unit::Tib, Unit::Second)),
153        "pibps" => Some(rate_unit!(Unit::PiB, Unit::Second)), // Exception: PiB default to bytes
154        "eibps" => Some(rate_unit!(Unit::EiB, Unit::Second)), // Exception: EiB default to bytes
155
156        "req" | "request" | "requests" => Some(Unit::Request),
157        "query" | "queries" => Some(Unit::Query),
158
159        "req/s" | "requests/s" | "rps" => Some(Unit::RateUnit(
160            Box::new(Unit::Request),
161            Box::new(Unit::Second),
162        )),
163        "req/min" | "req/minute" | "requests/min" | "requests/minute" | "rpm" => {
164            Some(rate_unit!(Unit::Request, Unit::Minute))
165        }
166        "req/h" | "req/hour" | "requests/h" | "requests/hour" | "rph" => Some(Unit::RateUnit(
167            Box::new(Unit::Request),
168            Box::new(Unit::Hour),
169        )),
170        "qps" | "queries/s" | "queries/sec" => Some(Unit::RateUnit(
171            Box::new(Unit::Query),
172            Box::new(Unit::Second),
173        )),
174        "qpm" | "queries/min" | "queries/minute" => Some(Unit::RateUnit(
175            Box::new(Unit::Query),
176            Box::new(Unit::Minute),
177        )),
178        "qph" | "queries/h" | "queries/hour" => Some(rate_unit!(Unit::Query, Unit::Hour)),
179
180        "%" | "percent" | "percentage" => Some(Unit::Percent),
181
182        // Currency symbols and codes
183        "$" | "usd" | "dollar" | "dollars" => Some(Unit::USD),
184        "€" | "eur" | "euro" | "euros" => Some(Unit::EUR),
185        "£" | "gbp" | "pound" | "pounds" | "sterling" => Some(Unit::GBP),
186        "¥" | "jpy" | "yen" => Some(Unit::JPY),
187        "cny" | "yuan" | "rmb" => Some(Unit::CNY),
188        "c$" | "cad" | "canadian" => Some(Unit::CAD),
189        "a$" | "aud" | "australian" => Some(Unit::AUD),
190        "chf" | "franc" => Some(Unit::CHF),
191        "₹" | "inr" | "rupee" | "rupees" => Some(Unit::INR),
192        "₩" | "krw" | "won" => Some(Unit::KRW),
193
194        _ => {
195            let mut rate_type = None;
196            if let Some(slash_pos) = text.find('/') {
197                // Use split_at which ensures we split at a valid char boundary
198                let (left_part, right_part) = text.split_at(slash_pos);
199                // Skip the '/' character safely
200                if let Some(right_part) = right_part.strip_prefix('/') {
201                    let left_unit = parse_unit(left_part);
202                    let right_unit = parse_unit(right_part);
203                    if let (Some(left_unit), Some(right_unit)) = (left_unit, right_unit) {
204                        if right_unit.unit_type() == UnitType::Time {
205                            rate_type = Some(rate_unit!(left_unit, right_unit))
206                        }
207                    }
208                }
209            }
210            rate_type
211        }
212    }
213}