├── .editorconfig ├── .gitattributes ├── .github ├── CODEOWNERS ├── dependabot.yaml └── workflows │ ├── build.yaml │ └── license.yaml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md └── src └── lib.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | max_line_length = 120 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | # Source code 4 | *.bash text eol=lf 5 | *.bat text eol=crlf 6 | *.c text diff=cpp 7 | *.cc text diff=cpp 8 | *.cmd text eol=crlf 9 | *.coffee text 10 | *.cpi text diff=cpp 11 | *.cpp text diff=cpp 12 | *.css text diff=css 13 | *.cxx text diff=cpp 14 | *.c++ text diff=cpp 15 | *.h text diff=cpp 16 | *.hh text diff=cpp 17 | *.hpp text diff=cpp 18 | *.htm text diff=html 19 | *.html text diff=html 20 | *.h++ text diff=cpp 21 | *.inc text 22 | *.ini text 23 | *.js text 24 | *.json text 25 | *.jsx text 26 | *.less text 27 | *.ls text 28 | *.map text -diff 29 | *.od text 30 | *.onlydata text 31 | *.php text diff=php 32 | *.pl text 33 | *.ps1 text eol=crlf 34 | *.py text diff=python 35 | *.rb text diff=ruby 36 | *.rs text diff=rust 37 | *.sass text 38 | *.scm text 39 | *.scss text diff=css 40 | *.sh text eol=lf 41 | *.sql text 42 | *.styl text 43 | *.tag text 44 | *.ts text 45 | *.tsx text 46 | *.xml text 47 | *.xhtml text diff=html 48 | 49 | # Compiled Object files 50 | *.slo binary 51 | *.lo binary 52 | *.o binary 53 | *.obj binary 54 | 55 | # Precompiled Headers 56 | *.gch binary 57 | *.pch binary 58 | 59 | # Compiled Dynamic libraries 60 | *.so binary 61 | *.dylib binary 62 | *.dll binary 63 | 64 | # Compiled Static libraries 65 | *.lai binary 66 | *.la binary 67 | *.a binary 68 | *.lib binary 69 | 70 | # Docker 71 | Dockerfile text 72 | 73 | # Documentation 74 | *.ipynb text 75 | *.markdown text diff=markdown 76 | *.md text diff=markdown 77 | *.mdwn text diff=markdown 78 | *.mdown text diff=markdown 79 | *.mkd text diff=markdown 80 | *.mkdn text diff=markdown 81 | *.mdtxt text 82 | *.mdtext text 83 | *.txt text 84 | AUTHORS text 85 | CHANGELOG text 86 | CHANGES text 87 | CONTRIBUTING text 88 | COPYING text 89 | copyright text 90 | *COPYRIGHT* text 91 | INSTALL text 92 | license text 93 | LICENSE text 94 | NEWS text 95 | readme text 96 | *README* text 97 | TODO text 98 | 99 | # Templates 100 | *.dot text 101 | *.ejs text 102 | *.erb text 103 | *.haml text 104 | *.handlebars text 105 | *.hbs text 106 | *.hbt text 107 | *.jade text 108 | *.latte text 109 | *.mustache text 110 | *.njk text 111 | *.phtml text 112 | *.svelte text 113 | *.tmpl text 114 | *.tpl text 115 | *.twig text 116 | *.vue text 117 | 118 | # Configs 119 | Cargo.lock text 120 | *.cnf text 121 | *.conf text 122 | *.config text 123 | .editorconfig text 124 | .env text 125 | .gitattributes text 126 | .gitconfig text 127 | .htaccess text 128 | *.lock text -diff 129 | package.json text eol=lf 130 | package-lock.json text -diff 131 | pnpm-lock.yaml text eol=lf -diff 132 | .prettierrc text 133 | yarn.lock text -diff 134 | *.toml text diff=toml 135 | *.yaml text 136 | *.yml text 137 | browserslist text 138 | Makefile text 139 | makefile text 140 | 141 | # Heroku 142 | Procfile text 143 | 144 | # Graphics 145 | *.ai binary 146 | *.bmp binary 147 | *.eps binary 148 | *.gif binary 149 | *.gifv binary 150 | *.ico binary 151 | *.jng binary 152 | *.jp2 binary 153 | *.jpg binary 154 | *.jpeg binary 155 | *.jpx binary 156 | *.jxr binary 157 | *.pdf binary 158 | *.png binary 159 | *.psb binary 160 | *.psd binary 161 | *.svg text 162 | *.svgz binary 163 | *.tif binary 164 | *.tiff binary 165 | *.wbmp binary 166 | *.webp binary 167 | 168 | # Audio 169 | *.kar binary 170 | *.m4a binary 171 | *.mid binary 172 | *.midi binary 173 | *.mp3 binary 174 | *.ogg binary 175 | *.ra binary 176 | 177 | # Video 178 | *.3gpp binary 179 | *.3gp binary 180 | *.as binary 181 | *.asf binary 182 | *.asx binary 183 | *.avi binary 184 | *.fla binary 185 | *.flv binary 186 | *.m4v binary 187 | *.mng binary 188 | *.mov binary 189 | *.mp4 binary 190 | *.mpeg binary 191 | *.mpg binary 192 | *.ogv binary 193 | *.swc binary 194 | *.swf binary 195 | *.webm binary 196 | 197 | # Archives 198 | *.7z binary 199 | *.gz binary 200 | *.jar binary 201 | *.rar binary 202 | *.tar binary 203 | *.zip binary 204 | 205 | # Fonts 206 | *.ttf binary 207 | *.eot binary 208 | *.otf binary 209 | *.woff binary 210 | *.woff2 binary 211 | 212 | # Executables 213 | *.exe binary 214 | *.out binary 215 | *.app binary 216 | 217 | # RC files 218 | *.*rc text 219 | 220 | # Ignore files 221 | *.*ignore text 222 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @RonniSkansing -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | # Make sure CI fails on all warnings, including Clippy lints 12 | RUSTFLAGS: "-Dwarnings" 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Lint 21 | run: | 22 | cargo fmt --all -- --check 23 | cargo clippy --all-targets --all-features 24 | - name: Build 25 | run: cargo build --verbose --all-features 26 | - name: Run tests 27 | run: cargo test --verbose --all-features 28 | -------------------------------------------------------------------------------- /.github/workflows/license.yaml: -------------------------------------------------------------------------------- 1 | name: license 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 1 1 *" 6 | 7 | concurrency: 8 | group: ${{ github.workflow }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | license: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - uses: FantasticFiasco/action-update-license-year@v3 21 | with: 22 | token: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo will have compiled files and executables 2 | /debug/ 3 | /target/ 4 | 5 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 6 | Cargo.lock 7 | 8 | # These are backup files generated by rustfmt 9 | **/*.rs.bk 10 | 11 | # MSVC Windows builds of rustc generate these, which store debugging information 12 | *.pdb 13 | 14 | # Log 15 | *.log 16 | 17 | # JetBrains 18 | /.idea/ 19 | 20 | # Visual Studio Code 21 | /.vscode/* 22 | !/.vscode/settings.json 23 | !/.vscode/tasks.json 24 | !/.vscode/launch.json 25 | !/.vscode/extensions.json 26 | !/.vscode/*.code-snippets 27 | 28 | # Local History for Visual Studio Code 29 | /.history/ 30 | 31 | # Built Visual Studio Code Extensions 32 | *.vsix 33 | 34 | # Converage 35 | /coverage/ 36 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "EditorConfig.EditorConfig", 4 | "GitHub.vscode-github-actions", 5 | "Gruntfuggly.todo-tree", 6 | "rust-lang.rust-analyzer", 7 | "serayuzgur.crates", 8 | "tamasfe.even-better-toml", 9 | "usernamehw.errorlens", 10 | "vadimcn.vscode-lldb" 11 | ] 12 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "rust-analyzer.check.command": "clippy", 4 | "[rust]": { 5 | "editor.defaultFormatter": "rust-lang.rust-analyzer" 6 | }, 7 | "[markdown]": { 8 | "editor.wordWrap": "off" 9 | } 10 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "duration-string" 3 | description = "String to duration and vice-versa lib. Format is [0-9]+(ns|us|ms|[smhdwy]) such as 100ms, 1s, 2h, 1y" 4 | documentation = "https://docs.rs/duration-string" 5 | readme = "README.md" 6 | repository = "https://github.com/Ronniskansing/duration-string" 7 | license-file = "LICENSE" 8 | keywords = ["duration"] 9 | categories = ["date-and-time"] 10 | exclude = ["coverage/**/*"] 11 | version = "0.5.2" 12 | authors = [ 13 | "Ronni Skansing ", 14 | "Martin Davy ", 15 | "Philip Sequeira ", 16 | "Kiran Ostrolenk ", 17 | "Carlo Corradini ", 18 | ] 19 | edition = "2018" 20 | 21 | [features] 22 | # default = ["serde"] 23 | 24 | [dependencies] 25 | serde = { version = "1.0.105", optional = true, features = ["derive"] } 26 | 27 | [dev-dependencies] 28 | serde_json = { version = "1.0.49" } 29 | 30 | [profile.test] 31 | 32 | [lints.rust] 33 | unsafe_code = "forbid" 34 | 35 | [lints.clippy] 36 | all = { level = "deny", priority = -1 } 37 | pedantic = { level = "deny", priority = -1 } 38 | cargo = { level = "deny", priority = -1 } 39 | 40 | 41 | # testing and doc'ing 42 | # cargo install cargo-tarpaulin 43 | # cargo install cargo-watch 44 | 45 | # cargo watch -x "tarpaulin --run-types Tests --out Lcov --output-dir coverage" 46 | # cargo watch -x "tarpaulin --run-types Tests --out Lcov --output-dir coverage; cargo test --doc; cargo doc" # VSCODE - Coverage Gutters 47 | 48 | # update readme 49 | # cargo install cargo-readme 50 | # cargo readme > readme.md 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2025 Tokio Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | cargo test --features="serde" 3 | install-dev: 4 | cargo install cargo-tarpaulin 5 | cargo install cargo-watch 6 | cargo install cargo-readme 7 | generate-docs: 8 | cargo readme > readme.md 9 | dev-mode-vsc: 10 | cargo watch -x "tarpaulin --run-types Tests --out Lcov --output-dir coverage; cargo test --doc; cargo doc" # VSCODE - Coverage Gutters 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # duration-string 2 | 3 | `duration-string` is a library to convert from `String` to `Duration` and vice-versa. 4 | 5 | Uses zero dependencies unless `serde` feature is enabled. 6 | 7 | [![build](https://github.com/RonniSkansing/duration-string/actions/workflows/build.yaml/badge.svg)](https://github.com/RonniSkansing/duration-string/actions/workflows/build.yaml) 8 | ![Crates.io](https://img.shields.io/crates/v/duration-string.svg) 9 | 10 | Takes a `String` such as `100ms`, `2s`, `5m 30s`, `1h10m` and converts it into a `Duration`. 11 | 12 | Takes a `Duration` and converts it into `String`. 13 | 14 | The `String` format is a multiply of `[0-9]+(ns|us|ms|[smhdwy])` 15 | 16 | ## Example 17 | 18 | `String` to `Duration`: 19 | 20 | ```rust 21 | use std::convert::TryFrom; 22 | use duration_string::DurationString; 23 | use std::time::Duration; 24 | 25 | let d: Duration = DurationString::try_from(String::from("100ms")).unwrap().into(); 26 | assert_eq!(d, Duration::from_millis(100)); 27 | 28 | // Alternatively 29 | let d: Duration = "100ms".parse::().unwrap().into(); 30 | assert_eq!(d, Duration::from_millis(100)); 31 | ``` 32 | 33 | `Duration` to `String`: 34 | 35 | ```rust 36 | use std::convert::TryFrom; 37 | use duration_string::*; 38 | use std::time::Duration; 39 | 40 | let d: String = DurationString::from(Duration::from_millis(100)).into(); 41 | assert_eq!(d, String::from("100ms")); 42 | ``` 43 | 44 | ## Serde support 45 | 46 | You can enable _serialization/deserialization_ support by adding the feature `serde` 47 | 48 | - Add `serde` feature 49 | 50 | ```toml 51 | duration-string = { version = "0.5.2", features = ["serde"] } 52 | ``` 53 | 54 | - Add derive to struct 55 | 56 | ```rust 57 | use duration_string::DurationString; 58 | use serde::{Deserialize, Serialize}; 59 | 60 | #[derive(Serialize, Deserialize)] 61 | struct Foo { 62 | duration: DurationString 63 | } 64 | ``` 65 | 66 | ## License 67 | 68 | This project is licensed under the [MIT](https://opensource.org/licenses/MIT) License. 69 | 70 | See [LICENSE](./LICENSE) file for details. 71 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! `duration-string` is a library to convert from `String` to `Duration` and vice-versa. 2 | //! 3 | //! ![Crates.io](https://img.shields.io/crates/v/duration-string.svg) 4 | //! ![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg) 5 | //! 6 | //! Takes a `String` such as `100ms`, `2s`, `5m 30s`, `1h10m` and converts it into a `Duration`. 7 | //! 8 | //! Takes a `Duration` and converts it into `String`. 9 | //! 10 | //! The `String` format is a multiply of `[0-9]+(ns|us|ms|[smhdwy])` 11 | //! 12 | //! ## Example 13 | //! 14 | //! `String` to `Duration`: 15 | //! 16 | //! ```rust 17 | //! use std::convert::TryFrom; 18 | //! use duration_string::DurationString; 19 | //! use std::time::Duration; 20 | //! 21 | //! let d: Duration = DurationString::try_from(String::from("100ms")).unwrap().into(); 22 | //! assert_eq!(d, Duration::from_millis(100)); 23 | //! 24 | //! // Alternatively 25 | //! let d: Duration = "100ms".parse::().unwrap().into(); 26 | //! assert_eq!(d, Duration::from_millis(100)); 27 | //! ``` 28 | //! 29 | //! `Duration` to `String`: 30 | //! 31 | //! ```rust 32 | //! use std::convert::TryFrom; 33 | //! use duration_string::*; 34 | //! use std::time::Duration; 35 | //! 36 | //! let d: String = DurationString::from(Duration::from_millis(100)).into(); 37 | //! assert_eq!(d, String::from("100ms")); 38 | //! ``` 39 | //! 40 | //! ## Serde support 41 | //! 42 | //! You can enable _serialization/deserialization_ support by adding the feature `serde` 43 | //! 44 | //! - Add `serde` feature 45 | //! 46 | //! ```toml 47 | //! duration-string = { version = "0.5.2", features = ["serde"] } 48 | //! ``` 49 | //! 50 | //! - Add derive to struct 51 | //! 52 | //! ```ignore 53 | //! use duration_string::DurationString; 54 | //! use serde::{Deserialize, Serialize}; 55 | //! 56 | //! #[derive(Serialize, Deserialize)] 57 | //! struct Foo { 58 | //! duration: DurationString 59 | //! } 60 | //! ``` 61 | //! 62 | #![cfg_attr(feature = "serde", doc = "```rust")] 63 | #![cfg_attr(not(feature = "serde"), doc = "```ignore")] 64 | //! ``` 65 | //! use duration_string::DurationString; 66 | //! use serde::{Deserialize, Serialize}; 67 | //! use serde_json; 68 | //! 69 | //! #[derive(Serialize, Deserialize)] 70 | //! struct SerdeSupport { 71 | //! t: DurationString, 72 | //! } 73 | //! let s = SerdeSupport { 74 | //! t: DurationString::from_string(String::from("1m")).unwrap(), 75 | //! }; 76 | //! assert_eq!(r#"{"t":"1m"}"#, serde_json::to_string(&s).unwrap()); 77 | //! ``` 78 | 79 | #[cfg(feature = "serde")] 80 | use serde::de::Unexpected; 81 | use std::borrow::{Borrow, BorrowMut}; 82 | use std::convert::TryFrom; 83 | #[cfg(feature = "serde")] 84 | use std::fmt; 85 | use std::iter::Sum; 86 | #[cfg(feature = "serde")] 87 | use std::marker::PhantomData; 88 | use std::num::ParseIntError; 89 | use std::ops::{Add, AddAssign, Deref, DerefMut, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; 90 | use std::str::FromStr; 91 | use std::time::Duration; 92 | 93 | const YEAR_IN_NANO: u128 = 31_556_926_000_000_000; 94 | const WEEK_IN_NANO: u128 = 604_800_000_000_000; 95 | const DAY_IN_NANO: u128 = 86_400_000_000_000; 96 | const HOUR_IN_NANO: u128 = 3_600_000_000_000; 97 | const MINUTE_IN_NANO: u128 = 60_000_000_000; 98 | const SECOND_IN_NANO: u128 = 1_000_000_000; 99 | const MILLISECOND_IN_NANO: u128 = 1_000_000; 100 | const MICROSECOND_IN_NANO: u128 = 1000; 101 | 102 | const HOUR_IN_SECONDS: u32 = 3600; 103 | const MINUTE_IN_SECONDS: u32 = 60; 104 | const DAY_IN_SECONDS: u32 = 86_400; 105 | const WEEK_IN_SECONDS: u32 = 604_800; 106 | const YEAR_IN_SECONDS: u32 = 31_556_926; 107 | 108 | pub type Result = std::result::Result; 109 | 110 | #[derive(Clone, Debug, Eq, PartialEq)] 111 | pub enum Error { 112 | Format, 113 | Overflow, 114 | ParseInt(ParseIntError), 115 | } 116 | 117 | impl std::fmt::Display for Error { 118 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 119 | match self { 120 | Self::Format => write!( 121 | f, 122 | "missing time duration format, must be multiples of `[0-9]+(ns|us|ms|[smhdwy])`" 123 | ), 124 | Self::Overflow => write!(f, "number is too large to fit in target type"), 125 | Self::ParseInt(err) => write!(f, "{err}"), 126 | } 127 | } 128 | } 129 | 130 | impl std::error::Error for Error { 131 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 132 | match self { 133 | Self::Format | Self::Overflow => None, 134 | Self::ParseInt(err) => Some(err), 135 | } 136 | } 137 | } 138 | 139 | impl From for Error { 140 | fn from(value: ParseIntError) -> Self { 141 | Self::ParseInt(value) 142 | } 143 | } 144 | 145 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Default)] 146 | pub struct DurationString(Duration); 147 | 148 | impl DurationString { 149 | #[must_use] 150 | pub const fn new(duration: Duration) -> DurationString { 151 | DurationString(duration) 152 | } 153 | 154 | #[allow(clippy::missing_errors_doc)] 155 | pub fn from_string(duration: String) -> Result { 156 | DurationString::try_from(duration) 157 | } 158 | } 159 | 160 | impl std::fmt::Display for DurationString { 161 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 162 | let s: String = (*self).into(); 163 | write!(f, "{s}") 164 | } 165 | } 166 | 167 | impl From for Duration { 168 | fn from(value: DurationString) -> Self { 169 | value.0 170 | } 171 | } 172 | 173 | impl From for String { 174 | fn from(value: DurationString) -> Self { 175 | let ns = value.0.as_nanos(); 176 | if ns % YEAR_IN_NANO == 0 { 177 | return (ns / YEAR_IN_NANO).to_string() + "y"; 178 | } 179 | if ns % WEEK_IN_NANO == 0 { 180 | return (ns / WEEK_IN_NANO).to_string() + "w"; 181 | } 182 | if ns % DAY_IN_NANO == 0 { 183 | return (ns / DAY_IN_NANO).to_string() + "d"; 184 | } 185 | if ns % HOUR_IN_NANO == 0 { 186 | return (ns / HOUR_IN_NANO).to_string() + "h"; 187 | } 188 | if ns % MINUTE_IN_NANO == 0 { 189 | return (ns / MINUTE_IN_NANO).to_string() + "m"; 190 | } 191 | if ns % SECOND_IN_NANO == 0 { 192 | return (ns / SECOND_IN_NANO).to_string() + "s"; 193 | } 194 | if ns % MILLISECOND_IN_NANO == 0 { 195 | return (ns / MILLISECOND_IN_NANO).to_string() + "ms"; 196 | } 197 | if ns % MICROSECOND_IN_NANO == 0 { 198 | return (ns / MICROSECOND_IN_NANO).to_string() + "us"; 199 | } 200 | ns.to_string() + "ns" 201 | } 202 | } 203 | 204 | impl From for DurationString { 205 | fn from(duration: Duration) -> Self { 206 | DurationString(duration) 207 | } 208 | } 209 | 210 | impl TryFrom for DurationString { 211 | type Error = Error; 212 | 213 | fn try_from(duration: String) -> std::result::Result { 214 | duration.parse() 215 | } 216 | } 217 | 218 | impl FromStr for DurationString { 219 | type Err = Error; 220 | 221 | fn from_str(duration: &str) -> std::result::Result { 222 | let duration: Vec = duration.chars().filter(|c| !c.is_whitespace()).collect(); 223 | let mut grouped_durations: Vec<(Vec, Vec)> = vec![(vec![], vec![])]; 224 | for i in 0..duration.len() { 225 | // Vector initialised with a starting element so unwraps should never panic 226 | if duration[i].is_numeric() { 227 | grouped_durations.last_mut().unwrap().0.push(duration[i]); 228 | } else { 229 | grouped_durations.last_mut().unwrap().1.push(duration[i]); 230 | } 231 | if i != duration.len() - 1 && !duration[i].is_numeric() && duration[i + 1].is_numeric() 232 | { 233 | // move to next group 234 | grouped_durations.push((vec![], vec![])); 235 | } 236 | } 237 | if grouped_durations.is_empty() { 238 | // `duration` either contains no numbers or no letters 239 | return Err(Error::Format); 240 | } 241 | let mut total_duration = Duration::new(0, 0); 242 | for (period, format) in grouped_durations { 243 | let period = match period.iter().collect::().parse::() { 244 | Ok(period) => Ok(period), 245 | Err(err) => Err(Error::ParseInt(err)), 246 | }?; 247 | let multiply_period = |multiplier: u32| -> std::result::Result { 248 | Duration::from_secs(period) 249 | .checked_mul(multiplier) 250 | .ok_or(Error::Overflow) 251 | }; 252 | let period_duration = match format.iter().collect::().as_ref() { 253 | "ns" => Ok(Duration::from_nanos(period)), 254 | "us" => Ok(Duration::from_micros(period)), 255 | "ms" => Ok(Duration::from_millis(period)), 256 | "s" => Ok(Duration::from_secs(period)), 257 | "m" => multiply_period(MINUTE_IN_SECONDS), 258 | "h" => multiply_period(HOUR_IN_SECONDS), 259 | "d" => multiply_period(DAY_IN_SECONDS), 260 | "w" => multiply_period(WEEK_IN_SECONDS), 261 | "y" => multiply_period(YEAR_IN_SECONDS), 262 | _ => Err(Error::Format), 263 | }?; 264 | total_duration = total_duration 265 | .checked_add(period_duration) 266 | .ok_or(Error::Overflow)?; 267 | } 268 | Ok(DurationString(total_duration)) 269 | } 270 | } 271 | 272 | impl Deref for DurationString { 273 | type Target = Duration; 274 | 275 | fn deref(&self) -> &Self::Target { 276 | &self.0 277 | } 278 | } 279 | 280 | impl DerefMut for DurationString { 281 | fn deref_mut(&mut self) -> &mut Self::Target { 282 | &mut self.0 283 | } 284 | } 285 | 286 | impl Borrow for DurationString { 287 | fn borrow(&self) -> &Duration { 288 | &self.0 289 | } 290 | } 291 | 292 | impl BorrowMut for DurationString { 293 | fn borrow_mut(&mut self) -> &mut Duration { 294 | &mut self.0 295 | } 296 | } 297 | 298 | impl PartialEq for DurationString { 299 | fn eq(&self, other: &Duration) -> bool { 300 | self.0.eq(other) 301 | } 302 | } 303 | 304 | impl PartialEq for Duration { 305 | fn eq(&self, other: &DurationString) -> bool { 306 | self.eq(&other.0) 307 | } 308 | } 309 | 310 | impl PartialOrd for DurationString { 311 | fn partial_cmp(&self, other: &Duration) -> Option { 312 | self.0.partial_cmp(other) 313 | } 314 | } 315 | 316 | impl PartialOrd for Duration { 317 | fn partial_cmp(&self, other: &DurationString) -> Option { 318 | self.partial_cmp(&other.0) 319 | } 320 | } 321 | 322 | impl Add for DurationString { 323 | type Output = Self; 324 | 325 | fn add(self, other: Self) -> Self::Output { 326 | Self::new(self.0.add(other.0)) 327 | } 328 | } 329 | 330 | impl Add for DurationString { 331 | type Output = Self; 332 | 333 | fn add(self, other: Duration) -> Self::Output { 334 | Self::new(self.0.add(other)) 335 | } 336 | } 337 | 338 | impl Add for Duration { 339 | type Output = Self; 340 | 341 | fn add(self, other: DurationString) -> Self::Output { 342 | self.add(other.0) 343 | } 344 | } 345 | 346 | impl AddAssign for DurationString { 347 | fn add_assign(&mut self, other: Self) { 348 | self.0.add_assign(other.0); 349 | } 350 | } 351 | 352 | impl AddAssign for DurationString { 353 | fn add_assign(&mut self, other: Duration) { 354 | self.0.add_assign(other); 355 | } 356 | } 357 | 358 | impl AddAssign for Duration { 359 | fn add_assign(&mut self, other: DurationString) { 360 | self.add_assign(other.0); 361 | } 362 | } 363 | 364 | impl Sub for DurationString { 365 | type Output = Self; 366 | 367 | fn sub(self, other: Self) -> Self::Output { 368 | Self::new(self.0.sub(other.0)) 369 | } 370 | } 371 | 372 | impl Sub for DurationString { 373 | type Output = Self; 374 | 375 | fn sub(self, other: Duration) -> Self::Output { 376 | Self::new(self.0.sub(other)) 377 | } 378 | } 379 | 380 | impl Sub for Duration { 381 | type Output = Self; 382 | 383 | fn sub(self, other: DurationString) -> Self::Output { 384 | self.sub(other.0) 385 | } 386 | } 387 | 388 | impl SubAssign for DurationString { 389 | fn sub_assign(&mut self, other: Self) { 390 | self.0.sub_assign(other.0); 391 | } 392 | } 393 | 394 | impl SubAssign for DurationString { 395 | fn sub_assign(&mut self, other: Duration) { 396 | self.0.sub_assign(other); 397 | } 398 | } 399 | 400 | impl SubAssign for Duration { 401 | fn sub_assign(&mut self, other: DurationString) { 402 | self.sub_assign(other.0); 403 | } 404 | } 405 | 406 | impl Mul for DurationString { 407 | type Output = Self; 408 | 409 | fn mul(self, other: u32) -> Self::Output { 410 | Self::new(self.0.mul(other)) 411 | } 412 | } 413 | 414 | impl Mul for u32 { 415 | type Output = DurationString; 416 | 417 | fn mul(self, other: DurationString) -> Self::Output { 418 | DurationString::new(self.mul(other.0)) 419 | } 420 | } 421 | 422 | impl MulAssign for DurationString { 423 | fn mul_assign(&mut self, other: u32) { 424 | self.0.mul_assign(other); 425 | } 426 | } 427 | 428 | impl Div for DurationString { 429 | type Output = Self; 430 | 431 | fn div(self, other: u32) -> Self::Output { 432 | Self::new(self.0.div(other)) 433 | } 434 | } 435 | 436 | impl DivAssign for DurationString { 437 | fn div_assign(&mut self, other: u32) { 438 | self.0.div_assign(other); 439 | } 440 | } 441 | 442 | impl Sum for DurationString { 443 | fn sum>(iter: I) -> Self { 444 | Self::new(Duration::sum(iter.map(|duration_string| duration_string.0))) 445 | } 446 | } 447 | 448 | impl<'a> Sum<&'a DurationString> for DurationString { 449 | fn sum>(iter: I) -> Self { 450 | Self::new(Duration::sum( 451 | iter.map(|duration_string| &duration_string.0), 452 | )) 453 | } 454 | } 455 | 456 | #[cfg(feature = "serde")] 457 | struct DurationStringVisitor { 458 | marker: PhantomData DurationString>, 459 | } 460 | 461 | #[cfg(feature = "serde")] 462 | impl DurationStringVisitor { 463 | fn new() -> Self { 464 | Self { 465 | marker: PhantomData, 466 | } 467 | } 468 | } 469 | 470 | #[cfg(feature = "serde")] 471 | #[allow(clippy::needless_lifetimes)] 472 | impl<'de> serde::de::Visitor<'de> for DurationStringVisitor { 473 | type Value = DurationString; 474 | 475 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 476 | formatter.write_str("string") 477 | } 478 | 479 | fn visit_str(self, string: &str) -> std::result::Result 480 | where 481 | E: serde::de::Error, 482 | { 483 | match DurationString::from_string(string.to_string()) { 484 | Ok(d) => Ok(d), 485 | Err(s) => Err(serde::de::Error::invalid_value( 486 | Unexpected::Str(&s.to_string()), 487 | &self, 488 | )), 489 | } 490 | } 491 | } 492 | 493 | #[cfg(feature = "serde")] 494 | impl<'de> serde::Deserialize<'de> for DurationString { 495 | fn deserialize(deserializer: D) -> std::result::Result 496 | where 497 | D: serde::Deserializer<'de>, 498 | { 499 | deserializer.deserialize_str(DurationStringVisitor::new()) 500 | } 501 | } 502 | 503 | #[cfg(feature = "serde")] 504 | impl serde::Serialize for DurationString { 505 | fn serialize(&self, serializer: S) -> std::result::Result 506 | where 507 | S: serde::Serializer, 508 | { 509 | serializer.serialize_str(&self.to_string()) 510 | } 511 | } 512 | #[cfg(test)] 513 | mod tests { 514 | use super::*; 515 | #[cfg(feature = "serde")] 516 | use serde::{Deserialize, Serialize}; 517 | 518 | #[cfg(feature = "serde")] 519 | #[derive(Serialize, Deserialize)] 520 | struct SerdeSupport { 521 | d: DurationString, 522 | } 523 | 524 | #[cfg(feature = "serde")] 525 | #[test] 526 | fn test_serialize_trait() { 527 | let s = SerdeSupport { 528 | d: DurationString::from_string(String::from("1m")).unwrap(), 529 | }; 530 | assert_eq!(r#"{"d":"1m"}"#, serde_json::to_string(&s).unwrap()); 531 | } 532 | 533 | #[cfg(feature = "serde")] 534 | #[test] 535 | fn test_deserialize_trait() { 536 | let s = r#"{"d":"2m"}"#; 537 | match serde_json::from_str::(s) { 538 | Ok(v) => { 539 | assert_eq!(v.d.to_string(), "2m"); 540 | } 541 | Err(err) => panic!("failed to deserialize: {}", err), 542 | } 543 | } 544 | 545 | #[test] 546 | fn test_string_int_overflow() { 547 | DurationString::from_string(String::from("ms")).expect_err("parsing \"ms\" should fail"); 548 | } 549 | 550 | #[test] 551 | fn test_from_string_no_char() { 552 | DurationString::from_string(String::from("1234")) 553 | .expect_err("parsing \"1234\" should fail"); 554 | } 555 | 556 | // fn test_from_string 557 | #[test] 558 | fn test_from_string() { 559 | let d = DurationString::from_string(String::from("100ms")); 560 | assert_eq!("100ms", format!("{}", d.unwrap())); 561 | } 562 | 563 | #[test] 564 | fn test_display_trait() { 565 | let d = DurationString::from(Duration::from_millis(100)); 566 | assert_eq!("100ms", format!("{d}")); 567 | } 568 | 569 | #[test] 570 | fn test_from_duration() { 571 | let d: String = DurationString::from(Duration::from_millis(100)).into(); 572 | assert_eq!(d, String::from("100ms")); 573 | } 574 | 575 | fn test_parse_string(input_str: &str, expected_duration: Duration) { 576 | let d_fromstr: Duration = input_str 577 | .parse::() 578 | .expect("Parse with FromStr failed") 579 | .into(); 580 | assert_eq!(d_fromstr, expected_duration, "FromStr"); 581 | let d_using_tryfrom: Duration = DurationString::try_from(input_str.to_owned()) 582 | .expect("Parse with TryFrom failed") 583 | .into(); 584 | assert_eq!(d_using_tryfrom, expected_duration, "TryFrom"); 585 | } 586 | 587 | #[test] 588 | fn test_from_string_ms() { 589 | test_parse_string("100ms", Duration::from_millis(100)); 590 | } 591 | 592 | #[test] 593 | fn test_from_string_us() { 594 | test_parse_string("100us", Duration::from_micros(100)); 595 | } 596 | 597 | #[test] 598 | fn test_from_string_us_ms() { 599 | test_parse_string("1ms100us", Duration::from_micros(1100)); 600 | } 601 | 602 | #[test] 603 | fn test_from_string_ns() { 604 | test_parse_string("100ns", Duration::from_nanos(100)); 605 | } 606 | 607 | #[test] 608 | fn test_from_string_s() { 609 | test_parse_string("1s", Duration::from_secs(1)); 610 | } 611 | 612 | #[test] 613 | fn test_from_string_m() { 614 | test_parse_string("1m", Duration::from_secs(60)); 615 | } 616 | 617 | #[test] 618 | fn test_from_string_m_s() { 619 | test_parse_string("1m 1s", Duration::from_secs(61)); 620 | } 621 | 622 | #[test] 623 | fn test_from_string_h() { 624 | test_parse_string("1h", Duration::from_secs(3600)); 625 | } 626 | 627 | #[test] 628 | fn test_from_string_h_m() { 629 | test_parse_string("1h30m", Duration::from_secs(5400)); 630 | } 631 | 632 | #[test] 633 | fn test_from_string_h_m2() { 634 | test_parse_string("1h128m", Duration::from_secs(11280)); 635 | } 636 | 637 | #[test] 638 | fn test_from_string_d() { 639 | test_parse_string("1d", Duration::from_secs(86_400)); 640 | } 641 | 642 | #[test] 643 | fn test_from_string_w() { 644 | test_parse_string("1w", Duration::from_secs(604_800)); 645 | } 646 | 647 | #[test] 648 | fn test_from_string_w_s() { 649 | test_parse_string("1w 1s", Duration::from_secs(604_801)); 650 | } 651 | 652 | #[test] 653 | fn test_from_string_y() { 654 | test_parse_string("1y", Duration::from_secs(31_556_926)); 655 | } 656 | 657 | #[test] 658 | fn test_into_string_ms() { 659 | let d: String = DurationString::try_from(String::from("100ms")) 660 | .unwrap() 661 | .into(); 662 | assert_eq!(d, "100ms"); 663 | } 664 | 665 | #[test] 666 | fn test_into_string_s() { 667 | let d: String = DurationString::try_from(String::from("1s")).unwrap().into(); 668 | assert_eq!(d, "1s"); 669 | } 670 | 671 | #[test] 672 | fn test_into_string_m() { 673 | let d: String = DurationString::try_from(String::from("1m")).unwrap().into(); 674 | assert_eq!(d, "1m"); 675 | } 676 | 677 | #[test] 678 | fn test_into_string_h() { 679 | let d: String = DurationString::try_from(String::from("1h")).unwrap().into(); 680 | assert_eq!(d, "1h"); 681 | } 682 | 683 | #[test] 684 | fn test_into_string_d() { 685 | let d: String = DurationString::try_from(String::from("1d")).unwrap().into(); 686 | assert_eq!(d, "1d"); 687 | } 688 | 689 | #[test] 690 | fn test_into_string_w() { 691 | let d: String = DurationString::try_from(String::from("1w")).unwrap().into(); 692 | assert_eq!(d, "1w"); 693 | } 694 | 695 | #[test] 696 | fn test_into_string_y() { 697 | let d: String = DurationString::try_from(String::from("1y")).unwrap().into(); 698 | assert_eq!(d, "1y"); 699 | } 700 | 701 | #[test] 702 | fn test_into_string_overflow_unit() { 703 | let d: String = DurationString::try_from(String::from("1000ms")) 704 | .unwrap() 705 | .into(); 706 | assert_eq!(d, "1s"); 707 | 708 | let d: String = DurationString::try_from(String::from("60000ms")) 709 | .unwrap() 710 | .into(); 711 | assert_eq!(d, "1m"); 712 | 713 | let d: String = DurationString::try_from(String::from("61000ms")) 714 | .unwrap() 715 | .into(); 716 | assert_eq!(d, "61s"); 717 | } 718 | 719 | #[test] 720 | fn test_from_string_invalid_string() { 721 | DurationString::try_from(String::from("1000x")) 722 | .expect_err("Should have failed with invalid format"); 723 | } 724 | 725 | #[test] 726 | fn test_try_from_string_overflow_y() { 727 | let result = DurationString::try_from(String::from("584554530873y")); 728 | assert_eq!(result, Err(Error::Overflow)); 729 | } 730 | 731 | #[test] 732 | fn test_try_from_string_overflow_y_w() { 733 | let result = DurationString::try_from(String::from("584554530872y 29w")); 734 | assert_eq!(result, Err(Error::Overflow)); 735 | } 736 | 737 | #[test] 738 | fn test_eq() { 739 | let duration = Duration::from_secs(1); 740 | assert_eq!(DurationString::new(duration), DurationString::new(duration)); 741 | assert_eq!(DurationString::new(duration), duration); 742 | assert_eq!(duration, DurationString::new(duration)); 743 | } 744 | 745 | #[test] 746 | fn test_ne() { 747 | let a = Duration::from_secs(1); 748 | let b = Duration::from_secs(2); 749 | assert_ne!(DurationString::new(a), DurationString::new(b)); 750 | assert_ne!(DurationString::new(a), b); 751 | assert_ne!(a, DurationString::new(b)); 752 | } 753 | 754 | #[test] 755 | fn test_lt() { 756 | let a = Duration::from_secs(1); 757 | let b = Duration::from_secs(2); 758 | assert!(DurationString::new(a) < DurationString::new(b)); 759 | assert!(DurationString::new(a) < b); 760 | assert!(a < DurationString::new(b)); 761 | } 762 | 763 | #[test] 764 | fn test_le() { 765 | let a = Duration::from_secs(1); 766 | let b = Duration::from_secs(2); 767 | assert!(DurationString::new(a) <= DurationString::new(b)); 768 | assert!(DurationString::new(a) <= b); 769 | assert!(a <= DurationString::new(b)); 770 | let a = Duration::from_secs(1); 771 | let b = Duration::from_secs(1); 772 | assert!(DurationString::new(a) <= DurationString::new(b)); 773 | assert!(DurationString::new(a) <= b); 774 | assert!(a <= DurationString::new(b)); 775 | } 776 | 777 | #[test] 778 | fn test_gt() { 779 | let a = Duration::from_secs(2); 780 | let b = Duration::from_secs(1); 781 | assert!(DurationString::new(a) > DurationString::new(b)); 782 | assert!(DurationString::new(a) > b); 783 | assert!(a > DurationString::new(b)); 784 | } 785 | 786 | #[test] 787 | fn test_ge() { 788 | let a = Duration::from_secs(2); 789 | let b = Duration::from_secs(1); 790 | assert!(DurationString::new(a) >= DurationString::new(b)); 791 | assert!(DurationString::new(a) >= b); 792 | assert!(a >= DurationString::new(b)); 793 | let a = Duration::from_secs(1); 794 | let b = Duration::from_secs(1); 795 | assert!(DurationString::new(a) >= DurationString::new(b)); 796 | assert!(DurationString::new(a) >= b); 797 | assert!(a >= DurationString::new(b)); 798 | } 799 | 800 | #[test] 801 | fn test_add() { 802 | let a = Duration::from_secs(1); 803 | let b = Duration::from_secs(1); 804 | let result = a + b; 805 | assert_eq!( 806 | DurationString::new(a) + DurationString::new(b), 807 | DurationString::new(result) 808 | ); 809 | assert_eq!(DurationString::new(a) + b, DurationString::new(result)); 810 | assert_eq!(a + DurationString::new(b), result); 811 | } 812 | 813 | #[test] 814 | fn test_add_assign() { 815 | let a = Duration::from_secs(1); 816 | let b = Duration::from_secs(1); 817 | let result = a + b; 818 | let mut duration_string_duration_string = DurationString::new(a); 819 | duration_string_duration_string += DurationString::new(b); 820 | let mut duration_string_duration = DurationString::new(a); 821 | duration_string_duration += b; 822 | let mut duration_duration_string = a; 823 | duration_duration_string += DurationString::new(b); 824 | assert_eq!(duration_string_duration_string, DurationString::new(result)); 825 | assert_eq!(duration_string_duration, DurationString::new(result)); 826 | assert_eq!(duration_duration_string, result); 827 | } 828 | 829 | #[test] 830 | fn test_sub() { 831 | let a = Duration::from_secs(1); 832 | let b = Duration::from_secs(1); 833 | let result = a - b; 834 | assert_eq!( 835 | DurationString::new(a) - DurationString::new(b), 836 | DurationString::new(result) 837 | ); 838 | assert_eq!(DurationString::new(a) - b, DurationString::new(result)); 839 | assert_eq!(a - DurationString::new(b), result); 840 | } 841 | 842 | #[test] 843 | fn test_sub_assign() { 844 | let a = Duration::from_secs(1); 845 | let b = Duration::from_secs(1); 846 | let result = a - b; 847 | let mut duration_string_duration_string = DurationString::new(a); 848 | duration_string_duration_string -= DurationString::new(b); 849 | let mut duration_string_duration = DurationString::new(a); 850 | duration_string_duration -= b; 851 | let mut duration_duration_string = a; 852 | duration_duration_string -= DurationString::new(b); 853 | assert_eq!(duration_string_duration_string, DurationString::new(result)); 854 | assert_eq!(duration_string_duration, DurationString::new(result)); 855 | assert_eq!(duration_duration_string, result); 856 | } 857 | 858 | #[test] 859 | fn test_mul() { 860 | let a = 2u32; 861 | let a_duration = DurationString::new(Duration::from_secs(a.into())); 862 | let b = 4u32; 863 | let b_duration = DurationString::new(Duration::from_secs(b.into())); 864 | let result = DurationString::new(Duration::from_secs((a * b).into())); 865 | assert_eq!(a_duration * b, result); 866 | assert_eq!(a * b_duration, result); 867 | } 868 | 869 | #[test] 870 | fn test_mul_assign() { 871 | let a = 2u32; 872 | let b = 4u32; 873 | let result = DurationString::new(Duration::from_secs((a * b).into())); 874 | let mut duration_string_u32 = DurationString::new(Duration::from_secs(a.into())); 875 | duration_string_u32 *= b; 876 | assert_eq!(duration_string_u32, result); 877 | } 878 | 879 | #[test] 880 | fn test_div() { 881 | let a = 8u32; 882 | let a_duration = DurationString::new(Duration::from_secs(a.into())); 883 | let b = 4u32; 884 | let result = DurationString::new(Duration::from_secs((a / b).into())); 885 | assert_eq!(a_duration / b, result); 886 | } 887 | 888 | #[test] 889 | fn test_div_assign() { 890 | let a = 8u32; 891 | let b = 4u32; 892 | let result = DurationString::new(Duration::from_secs((a / b).into())); 893 | let mut duration_string_u32 = DurationString::new(Duration::from_secs(a.into())); 894 | duration_string_u32 /= b; 895 | assert_eq!(duration_string_u32, result); 896 | } 897 | 898 | #[test] 899 | fn test_sum() { 900 | let durations = [ 901 | Duration::from_secs(1), 902 | Duration::from_secs(2), 903 | Duration::from_secs(3), 904 | Duration::from_secs(4), 905 | Duration::from_secs(5), 906 | Duration::from_secs(6), 907 | Duration::from_secs(7), 908 | Duration::from_secs(8), 909 | Duration::from_secs(9), 910 | ]; 911 | let result = DurationString::new(durations.iter().sum()); 912 | let durations = durations 913 | .iter() 914 | .map(|duration| Into::::into(*duration)) 915 | .collect::>(); 916 | assert_eq!(durations.iter().sum::(), result); 917 | assert_eq!(durations.into_iter().sum::(), result); 918 | } 919 | } 920 | --------------------------------------------------------------------------------