peck_lib/
lib.rs

1pub mod error;
2pub mod tests;
3
4pub mod str {
5    pub mod consts {
6        pub const OS_PATH_SEPARATOR: &str = if cfg!(windows) { r"\" } else { "/" };
7        pub const NUMS_CHARSET: &str = r"0123456789";
8        pub const FLOAT_CHARSET: &str = r"0.-123456789";
9        pub const ABS_FLOAT_CHARSET: &str = r".0123456789";
10    }
11    ///safely truncate a string to n length
12    #[inline]
13    pub fn trunc(input: &str, length: u8) -> &str {
14        &input[..{
15            if length as usize > input.len() {
16                input.len()
17            } else {
18                length as usize
19            }
20        }]
21    }
22}
23
24pub mod usize {
25    pub mod consts {
26        pub const APPROX_CM_IN_ARC_SECOND: usize = 3087usize;
27        pub const ARC_SECONDS_IN_360_DEGREES: usize = 1296000usize;
28        pub const ARC_SECONDS_IN_360_DEGREES_INDEXED: usize = 1295999usize;
29        pub const ARC_SECONDS_IN_180_DEGREES: usize = 648000usize;
30        pub const ARC_SECONDS_IN_180_DEGREES_INDEXED: usize = 647999usize;
31    }
32}
33
34pub mod f64 {
35    use crate::error::{Message, Warning};
36
37    use self::consts::{DEG_TO_RAD, RAD_TO_DEG};
38
39    pub mod consts {
40        pub const LATITUDE_LIMIT: f64 = 90.0f64; //latitude (-90 to 90), lower = -LATITUDE_LIMIT, upper = LATITUDE_LIMIT
41        pub const LONGITUDE_LIMIT: f64 = 180.0f64; //longitude (-180 to 180), lower = -LONGITUDE_LIMIT, upper = LONGITUDE_LIMIT
42        pub const ARC_SECONDS_IN_360_DEGREES: f64 = 1296000.0f64;
43        pub const ARC_SECONDS_IN_180_DEGREES: f64 = 648000.0f64;
44        pub const EARTH_RADIUS_KM: f64 = 6378.137f64;
45        pub const EARTH_RADIUS_M: f64 = 6378137.0f64;
46        pub const DEG_TO_RAD: f64 = 0.017453292519943295f64;
47        pub const RAD_TO_DEG: f64 = 57.29577951308232f64;
48    }
49
50    ///right hand side of the decimal point exactly as it would display, it won't gain any apparent precision when removing the exponent which gives the type more precision
51    #[inline]
52    pub fn rhs_exact(input: f64) -> f64 {
53        let input_string: String = input.to_string();
54        if let Some((_, rhs_str)) = input_string.split_once('.') {
55            format!("0.{}", rhs_str).parse::<f64>().unwrap()
56        } else {
57            0.0f64
58        }
59    }
60
61    ///split left and right side of the decimal point
62    #[inline]
63    pub fn split(input: f64) -> (f64, f64) {
64        (input.trunc(), input.fract())
65    }
66
67    ///split left and right side of the decimal point absolute
68    #[inline]
69    pub fn split_abs(input: f64) -> (f64, f64) {
70        (input.abs().trunc(), input.abs().fract())
71    }
72
73    ///converting an implied degree value to radians
74    #[inline]
75    pub fn to_radians(input_degrees: f64) -> f64 {
76        input_degrees * DEG_TO_RAD
77    }
78
79    ///converting an implied radians value to degrees
80    #[inline]
81    pub fn to_degrees(input_radians: f64) -> f64 {
82        input_radians * RAD_TO_DEG
83    }
84
85    ///normalise a value between the minimum and maximum value it could be
86    #[inline]
87    pub fn normalise(input: f64, min: f64, max: f64) -> f64 {
88        (input - min) / (max - min)
89    }
90
91    ///get the index using a value normalised to the dimensions of your vector, array, or other storage indexed or sized structures
92    #[inline]
93    pub fn normalised_to_index(input: f64, max: usize) -> usize {
94        (max as f64 * input) as usize
95    }
96
97    ///shifts the value from (-90 to 90) to (0 to 180)
98    #[inline]
99    pub fn indexify_lat(lat: f64) -> f64 {
100        lat + 90.0f64
101    }
102
103    ///shifts the value from (-180 to 180) to (0 to 360)
104    #[inline]
105    pub fn indexify_long(long: f64) -> f64 {
106        long + 180.0f64
107    }
108
109    ///shifts lat value from (-90 to 90) to (0 to 180) and long value from (-180 to 180) to (0 to 360)
110    #[inline]
111    pub fn indexify_lat_long(lat: f64, long: f64) -> (f64, f64) {
112        (indexify_lat(lat), indexify_long(long))
113    }
114
115    ///can only truncate to 19 decimal places safely
116    ///a normalised (between 0-1) f64 value will have a maximum of 8 significant digits after the decimal place
117    #[inline]
118    pub fn trunc(input: f64, decimal_places: u8) -> f64 {
119        let factor: f64 = 10usize.pow(decimal_places as u32) as f64;
120        let output_abs: f64 = (input.abs() * factor).floor() / factor;
121        output_abs.copysign(input)
122    }
123
124    #[inline]
125    #[allow(clippy::nonminimal_bool)]
126    pub fn trunc_safe(input: f64, decimal_places: u8) -> Result<f64, Warning> {
127        let mut safe: bool = true;
128        let factor: f64 = 10usize.pow({
129            if !(decimal_places > 19u8) {
130                decimal_places as u32
131            } else {
132                safe = false;
133                19u32
134            }
135        }) as f64;
136        let output_abs: f64 = (input.abs() * factor).floor() / factor;
137        let output: f64 = output_abs.copysign(input);
138        match safe {
139            true => Ok(output),
140            false => {
141                //not actually an error, but Result requires Ok() or Err()
142                //it passes through an enumerated message which implements display
143                //it currently only warns that it could only truncate to 19 decimal places
144                //and returns it as such.
145                Err(Warning::F64(output, Message::Max19DecimalPlaces))
146            }
147        }
148    }
149
150    ///no rounding
151    ///a normalised (between 0-1) f64 value will have a maximum of 16 significant digits after the decimal place
152    #[inline]
153    pub fn trunc_exact(input: f64, decimal_places: u8) -> f64 {
154        let input_string: String = input.to_string();
155        if let Some((lhs_str, rhs_str)) = input_string.split_once('.') {
156            let rhs_string: String = rhs_str
157                .chars()
158                .into_iter()
159                .take(decimal_places as usize)
160                .collect();
161            format!("{}.{}", lhs_str, rhs_string)
162                .parse::<f64>()
163                .unwrap()
164        } else {
165            input.trunc()
166        }
167    }
168
169    #[inline]
170    pub fn approx_equal_f64(a: f64, b: f64, decimal_places: u8) -> bool {
171        let factor: f64 = 10usize.pow(decimal_places as u32) as f64;
172        (a * factor).trunc() == (b * factor).trunc()
173    }
174
175    #[inline]
176    #[allow(clippy::unnecessary_unwrap)]
177    ///infallible, but significantly slower, 633ns vs 37ns
178    pub fn approx_equal_infallible_f64(a: f64, b: f64, decimal_places: u8) -> bool {
179        //lhs short circuit
180        if a as isize != b as isize {
181            return false;
182        }
183
184        let a_string: String = a.to_string();
185        let b_string: String = b.to_string();
186
187        let a_string_split: Option<(&str, &str)> = a_string.split_once('.');
188        let b_string_split: Option<(&str, &str)> = b_string.split_once('.');
189        if a_string_split.is_some() && b_string_split.is_some() {
190            let (_, a_rhs) = a_string_split.unwrap();
191            let (_, b_rhs) = b_string_split.unwrap();
192            return crate::str::trunc(a_rhs, decimal_places)
193                == crate::str::trunc(b_rhs, decimal_places);
194        } else if a_string_split.is_none() && b_string_split.is_none() {
195            return true;
196        }
197
198        false
199    }
200}
201
202pub mod f32 {
203    use self::consts::{DEG_TO_RAD, RAD_TO_DEG};
204
205    pub mod consts {
206        pub const LATITUDE_LIMIT: f32 = 90.0f32; //latitude (-90 to 90), lower = -LATITUDE_LIMIT, upper = LATITUDE_LIMIT
207        pub const LONGITUDE_LIMIT: f32 = 180.0f32; //longitude (-180 to 180), lower = -LONGITUDE_LIMIT, upper = LONGITUDE_LIMIT
208        pub const ARC_SECONDS_IN_360_DEGREES: f32 = 1296000.0f32;
209        pub const ARC_SECONDS_IN_180_DEGREES: f32 = 648000.0f32;
210        pub const EARTH_RADIUS_KM: f32 = 6378.137f32;
211        pub const EARTH_RADIUS_M: f32 = 6378137.0f32;
212        pub const DEG_TO_RAD: f32 = 0.017453292f32;
213        pub const RAD_TO_DEG: f32 = 57.29578f32; //57.295776
214    }
215
216    ///right hand side of the decimal point exactly as it would display, it won't gain any apparent precision when removing the exponent which gives the type more precision
217    #[inline]
218    pub fn rhs_exact(input: f32) -> f32 {
219        let input_string: String = input.to_string();
220        if let Some((_, rhs_str)) = input_string.split_once('.') {
221            format!("0.{}", rhs_str).parse::<f32>().unwrap()
222        } else {
223            0.0f32
224        }
225    }
226
227    ///split left and right side of the decimal point
228    #[inline]
229    pub fn split(input: f32) -> (f32, f32) {
230        (input.trunc(), input.fract())
231    }
232
233    ///split left and right side of the decimal point absolute
234    #[inline]
235    pub fn split_abs(input: f32) -> (f32, f32) {
236        (input.abs().trunc(), input.abs().fract())
237    }
238
239    ///converting an implied degree value to radians
240    #[inline]
241    pub fn to_radians(input_degrees: f32) -> f32 {
242        input_degrees * DEG_TO_RAD
243    }
244
245    ///converting an implied radians value to degrees
246    #[inline]
247    pub fn to_degrees(input_radians: f32) -> f32 {
248        input_radians * RAD_TO_DEG
249    }
250
251    ///normalise a value between the minimum and maximum value it could be
252    #[inline]
253    pub fn normalise(input: f32, min: f32, max: f32) -> f32 {
254        (input - min) / (max - min)
255    }
256
257    ///get the index using a value normalised to the dimensions of your vector, array, or other storage indexed or sized structures
258    #[inline]
259    pub fn normalised_to_index(input: f32, max: usize) -> usize {
260        (max as f32 * input) as usize
261    }
262
263    ///shifts the value from (-90 to 90) to (0 to 180)
264    #[inline]
265    pub fn indexify_lat(lat: f32) -> f32 {
266        lat + 90.0
267    }
268
269    ///shifts the value from (-180 to 180) to (0 to 360)
270    #[inline]
271    pub fn indexify_long(long: f32) -> f32 {
272        long + 180.0
273    }
274
275    ///shifts lat value from (-90 to 90) to (0 to 180) and long value from (-180 to 180) to (0 to 360)
276    #[inline]
277    pub fn indexify_lat_long(lat: f32, long: f32) -> (f32, f32) {
278        (indexify_lat(lat), indexify_long(long))
279    }
280
281    ///can only truncate to 8 decimal places safely
282    ///a normalised (between 0-1) f64 value will have a maximum of 8 significant digits after the decimal place
283    #[inline]
284    pub fn trunc(input: f32, decimal_places: u8) -> f32 {
285        let factor: f32 = 10usize.pow(decimal_places as u32) as f32;
286        let output_abs: f32 = (input.abs() * factor).floor() / factor;
287        output_abs.copysign(input)
288    }
289
290    #[inline]
291    #[allow(clippy::nonminimal_bool)]
292    pub fn trunc_safe(input: f32, decimal_places: u8) -> Result<f32, crate::error::Warning> {
293        let mut safe: bool = true;
294        let factor: f32 = 10usize.pow({
295            if !(decimal_places > 19u8) {
296                decimal_places as u32
297            } else {
298                safe = false;
299                19u32
300            }
301        }) as f32;
302        let output_abs: f32 = (input.abs() * factor).floor() / factor;
303        let output: f32 = output_abs.copysign(input);
304        match safe {
305            true => Ok(output),
306            false => {
307                //not actually an error, but Result requires Ok() or Err()
308                //it passes through an enumerated message which implements display
309                //it currently only warns that it could only truncate to 19 decimal places
310                //and returns it as such.
311                Err(crate::error::Warning::F32(
312                    output,
313                    crate::error::Message::Max19DecimalPlaces,
314                ))
315            }
316        }
317    }
318
319    ///a normalised (between 0-1) f64 value will have a maximum of 16 values after the decimal place
320    ///no rounding
321    #[inline]
322    pub fn trunc_exact(input: f32, decimal_places: u8) -> f32 {
323        let input_string: String = input.to_string();
324        if let Some((lhs_str, rhs_str)) = input_string.split_once('.') {
325            let rhs_string: String = rhs_str
326                .chars()
327                .into_iter()
328                .take(decimal_places as usize)
329                .collect();
330            format!("{}.{}", lhs_str, rhs_string)
331                .parse::<f32>()
332                .unwrap()
333        } else {
334            input.trunc()
335        }
336    }
337
338    #[inline]
339    pub fn approx_equal_f32(a: f32, b: f32, decimal_places: u8) -> bool {
340        let factor: f32 = 10usize.pow(decimal_places as u32) as f32;
341        (a * factor).trunc() == (b * factor).trunc()
342    }
343
344    #[inline]
345    #[allow(clippy::unnecessary_unwrap)]
346    pub fn approx_equal_infallible_f32(a: f32, b: f32, decimal_places: u8) -> bool {
347        //lhs short circuit
348        if a as isize != b as isize {
349            return false;
350        }
351
352        let a_string: String = a.to_string();
353        let b_string: String = b.to_string();
354
355        let a_string_split: Option<(&str, &str)> = a_string.split_once('.');
356        let b_string_split: Option<(&str, &str)> = b_string.split_once('.');
357        if a_string_split.is_some() && b_string_split.is_some() {
358            let (_, a_rhs) = a_string_split.unwrap();
359            let (_, b_rhs) = b_string_split.unwrap();
360            return crate::str::trunc(a_rhs, decimal_places)
361                == crate::str::trunc(b_rhs, decimal_places);
362        } else if a_string_split.is_none() && b_string_split.is_none() {
363            return true;
364        }
365
366        false
367    }
368}