ms_converter/lib.rs
1/*!
2Fast abstraction for converting human-like times into milliseconds.
3
4There are two ways to calculate milliseconds:
5* In the runtime `crate::ms_converter::ms`
6* In the compilation time `crate::ms_converter::ms_expr`
7
8## Usage
9
10### Running ms converter in Runtime:
11```rust
12use crate::ms_converter::ms;
13
14let value = ms("1d").unwrap();
15assert_eq!(value, 86400000)
16```
17
18### Convert ms in the compilation step:
19```rust
20use crate::ms_converter::ms_expr;
21
22const VALUE: i64 = ms_expr!(i64, 1 d);
23assert_eq!(VALUE, 86400000)
24```
25
26### Convert ms into `time.Duration`
27```rust
28use crate::ms_converter::ms_into_time;
29
30let value = ms_into_time("1d").unwrap();
31assert_eq!(value.as_millis(), 86400000)
32```
33
34### Convert milliseconds into human-like time string
35```
36use crate::ms_converter::{get_duration_by_postfix, DAY};
37
38let value = get_duration_by_postfix(DAY as i64, " day").unwrap();
39assert_eq!(value, "1 day")
40```
41
42### Convert milliseconds into human-like time string without postfix
43```
44use crate::ms_converter::{get_max_possible_duration, DAY};
45
46let value = get_max_possible_duration(DAY as i64).unwrap();
47assert_eq!(value, "1d")
48```
49
50### Convert milliseconds into long human-like time string without postfix
51```
52use crate::ms_converter::{get_max_possible_duration_long, WEEK};
53
54let value = get_max_possible_duration_long(2 * WEEK as i64).unwrap();
55assert_eq!(value, "14 days") // Max possible period is a day
56```
57
58## Supported time strings
59* **Years:** `years`, `year`, `yrs`, `yr`, `y`
60* **Weeks:** `weeks`, `week`, `w`
61* **Days:** `days`, `day`, `d`
62* **Hours:** `hours`, `hour`, `hrs`, `hr`, `h`
63* **Minutes:** `minutes`, `minute`, `mins`, `min`, `m`
64* **Seconds:** `seconds`, `second`, `secs`, `sec`, `s`
65* **Milliseconds:** `milliseconds`, `millisecond`, `msecs`, `msec`, `ms` and empty postfix
66*/
67
68#![doc(issue_tracker_base_url = "https://github.com/Mnwa/ms/issues/")]
69#![doc(html_root_url = "https://docs.rs/ms-converter/")]
70#![no_std]
71
72extern crate std;
73
74use std::borrow::Cow;
75use std::fmt::Formatter;
76use std::format;
77use std::ops::{Add, Mul, Sub};
78use std::string::String;
79use std::time::Duration;
80
81/// How many milliseconds in one second
82pub const SECOND: f64 = 1000_f64;
83/// How many milliseconds in one minute
84pub const MINUTE: f64 = SECOND * 60_f64;
85/// How many milliseconds in one hour
86pub const HOUR: f64 = MINUTE * 60_f64;
87/// How many milliseconds in one day
88pub const DAY: f64 = HOUR * 24_f64;
89/// How many milliseconds in one week
90pub const WEEK: f64 = DAY * 7_f64;
91/// How many milliseconds in one year
92pub const YEAR: f64 = DAY * 365.25_f64;
93
94/// Fast abstraction for converting human-like times into milliseconds.
95/// `ms` function gets an str slice or String and returns how much milliseconds in your pattern.
96///
97/// ### Usage
98/// ```
99/// use crate::ms_converter::ms;
100///
101/// let value = ms("1d").unwrap();
102/// assert_eq!(value, 86400000)
103/// ```
104///
105/// ### Supported time strings
106/// * **Years:** `years`, `year`, `yrs`, `yr`, `y`
107/// * **Weeks:** `weeks`, `week`, `w`
108/// * **Days:** `days`, `day`, `d`
109/// * **Hours:** `hours`, `hour`, `hrs`, `hr`, `h`
110/// * **Minutes:** `minutes`, `minute`, `mins`, `min`, `m`
111/// * **Seconds:** `seconds`, `second`, `secs`, `sec`, `s`
112/// * **Milliseconds:** `milliseconds`, `millisecond`, `msecs`, `msec`, `ms` and empty postfix
113#[inline(always)]
114pub fn ms<'a, T>(s: T) -> Result<i64, Error>
115where
116 T: Into<Cow<'a, str>>,
117{
118 let s = &*s.into();
119
120 let (value, postfix): (&str, &str) = s
121 .find(|c: char| !matches!(c, '0'..='9' | '.' | '-' | '+'))
122 .map_or((s, ""), |vi| s.split_at(vi));
123
124 let postfix = get_byte_postfix(postfix);
125
126 parse(value.as_bytes())
127 .and_then(move |value| Ok(get_modification(postfix)? * value))
128 .map(|v| v.round() as i64)
129}
130
131/// Getting human-like time from milliseconds.
132/// `get_duration_by_postfix` function gets a milliseconds count and str slice or String as postfix
133/// and returns a string with your time.
134///
135/// ### Usage
136/// ```
137/// use crate::ms_converter::{get_duration_by_postfix, DAY};
138///
139/// let value = get_duration_by_postfix(1 * DAY as i64, "day").unwrap();
140/// assert_eq!(value, "1day")
141/// ```
142///
143/// You can add the space to start of you prefix to get space between date and postfix on return.
144/// ```
145/// use crate::ms_converter::{get_duration_by_postfix, DAY};
146///
147/// let value = get_duration_by_postfix(DAY as i64, " day").unwrap();
148/// assert_eq!(value, "1 day")
149/// ```
150/// also you can a pass negative values
151/// ```
152/// use crate::ms_converter::{get_duration_by_postfix, DAY};
153///
154/// let value = get_duration_by_postfix(-DAY as i64, " day").unwrap();
155/// assert_eq!(value, "-1 day")
156/// ```
157
158/// ### Supported postfixes
159/// * **Years:** `years`, `year`, `yrs`, `yr`, `y`
160/// * **Weeks:** `weeks`, `week`, `w`
161/// * **Days:** `days`, `day`, `d`
162/// * **Hours:** `hours`, `hour`, `hrs`, `hr`, `h`
163/// * **Minutes:** `minutes`, `minute`, `mins`, `min`, `m`
164/// * **Seconds:** `seconds`, `second`, `secs`, `sec`, `s`
165/// * **Milliseconds:** `milliseconds`, `millisecond`, `msecs`, `msec`, `ms` and empty postfix
166#[inline]
167pub fn get_duration_by_postfix<'a, P>(milliseconds: i64, postfix: P) -> Result<String, Error>
168where
169 P: Into<Cow<'a, str>>,
170{
171 let postfix = &*postfix.into();
172 let b_postfix = get_byte_postfix(postfix);
173 let v = get_modification(b_postfix)?;
174 Ok(format!(
175 "{}{}",
176 (milliseconds as f64 / v).round() as i64,
177 postfix
178 ))
179}
180
181/// Getting human-like time from milliseconds.
182/// `get_max_possible_duration` function gets a milliseconds count and returns a max possible string with your time.
183/// `get_max_possible_duration` **has some limitations** maximum of avalable postfixes is a day.
184///
185/// ### Usage
186/// ```
187/// use crate::ms_converter::{get_max_possible_duration, WEEK};
188///
189/// let value = get_max_possible_duration(2 * WEEK as i64).unwrap();
190/// assert_eq!(value, "14d") // Max possible period is a day
191/// ```
192///
193/// also you can a pass negative values
194/// ```
195/// use crate::ms_converter::{get_max_possible_duration, WEEK};
196///
197/// let value = get_max_possible_duration(-2 * WEEK as i64).unwrap();
198/// assert_eq!(value, "-14d") // Max possible period is a day
199/// ```
200#[inline]
201pub fn get_max_possible_duration(milliseconds: i64) -> Result<String, Error> {
202 let postfix = match milliseconds.abs() {
203 m if m >= DAY as i64 => "d",
204 m if m >= HOUR as i64 => "h",
205 m if m >= MINUTE as i64 => "m",
206 m if m >= SECOND as i64 => "s",
207 _ => "ms",
208 };
209 get_duration_by_postfix(milliseconds, postfix)
210}
211
212/// Getting human-like time from milliseconds.
213/// `get_max_possible_duration_long` function gets a milliseconds count and returns a max possible string with your time.
214/// `get_max_possible_duration_long` **has some limitations** maximum of avalable postfixes is a day.
215///
216/// ### Usage
217/// ```
218/// use crate::ms_converter::{get_max_possible_duration_long, WEEK};
219///
220/// let value = get_max_possible_duration_long(2 * WEEK as i64).unwrap();
221/// assert_eq!(value, "14 days") // Max possible period is a day
222/// ```
223///
224/// ```
225/// use crate::ms_converter::{get_max_possible_duration_long, DAY};
226///
227/// let value = get_max_possible_duration_long(DAY as i64).unwrap();
228/// assert_eq!(value, "1 day")
229/// ```
230///
231/// also you can a pass negative values
232/// ```
233/// use crate::ms_converter::{get_max_possible_duration_long, WEEK};
234///
235/// let value = get_max_possible_duration_long(-2 * WEEK as i64).unwrap();
236/// assert_eq!(value, "-14 days") // Max possible period is a day
237/// ```
238#[inline]
239pub fn get_max_possible_duration_long(milliseconds: i64) -> Result<String, Error> {
240 let postfix = match milliseconds.abs() {
241 m if m >= DAY as i64 => check_postfix(m, DAY, " day", " days"),
242 m if m >= HOUR as i64 => check_postfix(m, HOUR, " hour", " hours"),
243 m if m >= MINUTE as i64 => check_postfix(m, MINUTE, " minute", " minutes"),
244 m if m >= SECOND as i64 => check_postfix(m, SECOND, " second", " seconds"),
245 m => check_postfix(m, 1f64, " millisecond", " milliseconds"),
246 };
247 get_duration_by_postfix(milliseconds, postfix)
248}
249
250#[inline(always)]
251#[doc(hidden)]
252fn check_postfix<'a>(
253 milliseconds: i64,
254 period: f64,
255 postfix: &'a str,
256 postfix_mul: &'a str,
257) -> &'a str {
258 if milliseconds as f64 >= 1.5 * period {
259 return postfix_mul;
260 }
261 postfix
262}
263
264#[inline(always)]
265#[doc(hidden)]
266fn get_byte_postfix(postfix: &str) -> &[u8] {
267 let b_postfix = postfix.as_bytes();
268 match b_postfix.first() {
269 Some(c) if c.is_ascii_whitespace() => &b_postfix[1..],
270 _ => b_postfix,
271 }
272}
273
274#[inline(always)]
275#[doc(hidden)]
276fn get_modification(postfix: &[u8]) -> Result<f64, Error> {
277 match postfix.first() {
278 Some(b'y') if matches!(postfix, b"years" | b"year" | b"yrs" | b"yr" | b"y") => Ok(YEAR),
279 Some(b'w') if matches!(postfix, b"weeks" | b"week" | b"w") => Ok(WEEK),
280 Some(b'd') if matches!(postfix, b"days" | b"day" | b"d") => Ok(DAY),
281 Some(b'h') if matches!(postfix, b"hours" | b"hour" | b"hrs" | b"hr" | b"h") => Ok(HOUR),
282 Some(b'm') if matches!(postfix, b"minutes" | b"minute" | b"mins" | b"min" | b"m") => {
283 Ok(MINUTE)
284 }
285 None | Some(b'm')
286 if matches!(
287 postfix,
288 b"milliseconds" | b"millisecond" | b"msecs" | b"msec" | b"ms" | b""
289 ) =>
290 {
291 Ok(1f64)
292 }
293 Some(b's') if matches!(postfix, b"seconds" | b"second" | b"secs" | b"sec" | b"s") => {
294 Ok(SECOND)
295 }
296 _ => Err(Error::new("invalid postfix")),
297 }
298}
299
300#[inline(always)]
301#[doc(hidden)]
302fn parse(mut num: &[u8]) -> Result<f64, Error> {
303 let sign = match num.first() {
304 Some(b'-') => {
305 num = &num[1..];
306 -1_f64
307 }
308 Some(b'+') => {
309 num = &num[1..];
310 1_f64
311 }
312 _ => 1_f64,
313 };
314 let (mut ind, mut dist) = num
315 .iter()
316 .take_while(|b| b.is_ascii_digit())
317 .map(|b| b.sub(b'0') as f64)
318 .fold((0, 0_f64), |(ind, dist), b| {
319 (ind.add(1), dist.mul_add(10_f64, b))
320 });
321
322 if matches!(num.get(ind), Some(b'.')) {
323 ind = ind.add(1)
324 }
325
326 if ind < num.len() {
327 let (pow, temp) = num[ind..]
328 .iter()
329 .take_while(|b| b.is_ascii_digit())
330 .map(|b| b.sub(b'0') as f64)
331 .fold((1, 0_f64), |(pow, temp), b| {
332 (pow.add(1), temp.mul_add(10_f64, b))
333 });
334
335 ind = ind.add(pow as usize).sub(1);
336 let pow = pow.mul(-1_i32).add(1);
337 dist = dist.add(temp.mul((10_f64).powi(pow)));
338 }
339
340 if num.len() == ind {
341 Ok(dist.copysign(sign))
342 } else {
343 Err(Error::new("invalid value"))
344 }
345}
346
347/// Zero cost converter from human-like time into a number.
348/// In the first argument, you need to pass type of your number (`i64`, `f64` and etc).
349/// The second argument is human-time construction, like `1 day`, `2 h`.
350/// The output will be a number with type what you set in the first argument.
351///
352/// **This macro will be precalculated in compilation time.** Also, you can use ms_expr with constants:
353///
354/// ```
355/// use crate::ms_converter::ms_expr;
356///
357/// const VALUE: f64 = ms_expr!(f64, 2.5 hrs);
358/// assert_eq!(VALUE, 9000000.)
359/// ```
360///
361/// ### Usage
362/// ```
363/// use crate::ms_converter::ms_expr;
364///
365/// assert_eq!(ms_expr!(i64, 1 d), 86400000)
366/// ```
367#[macro_export]
368macro_rules! ms_expr {
369 ($type:ty, $x:literal $(milliseconds)?$(millisecond)?$(msecs)?$(msec)?$(ms)?) => {{
370 let x: $type = $x;
371 x
372 }};
373 ($type:ty, $x:literal $(seconds)?$(second)?$(secs)?$(sec)?$(s)?) => {{
374 let x: $type = $x * ($crate::SECOND as $type);
375 x
376 }};
377 ($type:ty, $x:literal $(minutes)?$(minute)?$(mins)?$(min)?$(m)?) => {{
378 let x: $type = $x * ($crate::MINUTE as $type);
379 x
380 }};
381 ($type:ty, $x:literal $(hours)?$(hour)?$(hrs)?$(hr)?$(h)?) => {{
382 let x: $type = $x * ($crate::HOUR as $type);
383 x
384 }};
385 ($type:ty, $x:literal $(days)?$(day)?$(d)?) => {{
386 let x: $type = $x * ($crate::DAY as $type);
387 x
388 }};
389 ($type:ty, $x:literal $(weeks)?$(week)?$(w)?) => {{
390 let x: $type = $x * ($crate::WEEK as $type);
391 x
392 }};
393 ($type:ty, $x:literal $(years)?$(year)?$(yrs)?$(yr)?$(y)?) => {{
394 let x: $type = $x * ($crate::YEAR as $type);
395 x
396 }};
397}
398
399/// Ms into time is the abstraction on `ms` function, which converts result into `time.Duration` type.
400/// `ms_into_time` function gets an str slice or String and returns `time.Duration`.
401/// `ms_into_time` **has some limitations**, it's not working with negative values:
402/// ```
403/// use crate::ms_converter::ms_into_time;
404///
405/// let value = ms_into_time("-1d").is_err();
406/// assert_eq!(value, true)
407/// ```
408///
409/// ### Usage
410/// ```
411/// use crate::ms_converter::ms_into_time;
412///
413/// let value = ms_into_time("1d").unwrap();
414/// assert_eq!(value.as_millis(), 86400000)
415/// ```
416pub fn ms_into_time<'a, T>(s: T) -> Result<Duration, Error>
417where
418 T: Into<Cow<'a, str>>,
419{
420 let milliseconds = ms(s)?;
421 if milliseconds < 0 {
422 return Err(Error::new("time.Duration cannot work with negative values"));
423 }
424 Ok(Duration::from_millis(milliseconds as u64))
425}
426
427/// Error which return `ms_converter` functions in runtime, if something is going wrong.
428#[derive(Debug)]
429pub struct Error {
430 message: &'static str,
431}
432
433impl Error {
434 pub fn new(message: &'static str) -> Error {
435 Error { message }
436 }
437}
438
439impl std::fmt::Display for Error {
440 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
441 write!(f, "{}", self.message)
442 }
443}
444
445impl std::error::Error for Error {}
446
447#[cfg(test)]
448mod tests;