diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef862 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +.idea/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2fecbd5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,408 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "atty" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", + "termion 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "autocfg" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "backtrace" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "backtrace-sys" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bitflags" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cc" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cfg-if" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "chrono" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "clap" +version = "2.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace 0.3.30 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure_derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.35 (registry+https://github.com/rust-lang/crates.io-index)", + "synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-segmentation 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "lazy_static" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.58" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memchr" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "num-integer" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "redox_syscall" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "redox_termios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "strum" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "strum_macros" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.35 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "syn" +version = "0.15.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "synstructure" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.35 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "termion" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", + "numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread_local" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "time" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "timedelta" +version = "0.1.0" +dependencies = [ + "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ucd-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-segmentation" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-width" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "ut" +version = "0.1.0" +dependencies = [ + "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "strum 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", + "strum_macros 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", + "timedelta 0.1.0", +] + +[[package]] +name = "utf8-ranges" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "vec_map" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e6f484ae0c99fec2e858eb6134949117399f222608d84cadb3f58c1f97c2364c" +"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" +"checksum autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "0e49efa51329a5fd37e7c79db4621af617cd4e3e5bc224939808d076077077bf" +"checksum backtrace 0.3.30 (registry+https://github.com/rust-lang/crates.io-index)" = "ada4c783bb7e7443c14e0480f429ae2cc99da95065aeab7ee1b81ada0419404f" +"checksum backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6" +"checksum bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd" +"checksum cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "39f75544d7bbaf57560d2168f28fd649ff9c76153874db88bdbdfd839b1a7e7d" +"checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" +"checksum chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878" +"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" +"checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" +"checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" +"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" +"checksum libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "6281b86796ba5e4366000be6e9e18bf35580adf9e63fbe2294aadb587613a319" +"checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39" +"checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09" +"checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" +"checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" +"checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +"checksum quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "faf4799c5d274f3868a4aae320a0a182cbd2baee377b378f080e16a23e9d80db" +"checksum redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)" = "12229c14a0f65c4f1cb046a3b52047cdd9da1f4b30f8a39c5063c8bae515e252" +"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" +"checksum regex 1.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "0b2f0808e7d7e4fb1cb07feb6ff2f4bc827938f24f8c2e6a3beb7370af544bdd" +"checksum regex-syntax 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "9d76410686f9e3a17f06128962e0ecc5755870bb890c34820c7af7f1db2e1d48" +"checksum rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "a7f4dccf6f4891ebcc0c39f9b6eb1a83b9bf5d747cb439ec6fba4f3b977038af" +"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +"checksum strum 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e5d1c33039533f051704951680f1adfd468fd37ac46816ded0d9ee068e60f05f" +"checksum strum_macros 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "47cd23f5c7dee395a00fa20135e2ec0fffcdfa151c56182966d7a3261343432e" +"checksum syn 0.15.35 (registry+https://github.com/rust-lang/crates.io-index)" = "641e117d55514d6d918490e47102f7e08d096fdde360247e4a10f7a91a8478d3" +"checksum synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "02353edf96d6e4dc81aea2d8490a7e9db177bf8acb0e951c24940bf866cb313f" +"checksum termion 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dde0593aeb8d47accea5392b39350015b5eccb12c0d98044d856983d89548dea" +"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" +"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" +"checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86" +"checksum unicode-segmentation 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1967f4cdfc355b37fd76d2a954fb2ed3871034eb4f26d60537d88795cfc332a9" +"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" +"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +"checksum utf8-ranges 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9d50aa7650df78abf942826607c62468ce18d9019673d4a2ebe1865dbb96ffde" +"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" +"checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d72c3e7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ut" +version = "0.1.0" +authors = ["yoshihitoh "] +edition = "2018" + +[workspace] +members = [ + "timedelta", +] + +[dependencies] +chrono = "^0.4" +clap = "^2.33" +failure = "^0.1" +regex = "^1" +strum = "^0.15" +strum_macros = "^0.15" +timedelta = {version = "^0.1.0", "path" = "timedelta"} +lazy_static = "^1.3" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..031f328 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Yoshihito + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b550ca4 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +ut +---- + +ut is a command line tool to handle a unix timestamp. + +### Installation + +clone the repository and build it. + +``` bash +$ git clone https://github.com/yoshihitoh/ut +$ cd ut +$ cargo build --release +$ ./target/relase ut --version +ut 0.1.0 +``` + +### Usage + +#### Generate a unix timestamp + +Generate a unix timestamp of the midnight of today. +``` bash +$ ut generate -b today +1560870000 + +# You can use `-p` option to show it in millisecond. +$ ut generate -b today -p ms +1560870000000 +``` + +You can specify time deltas with `-d` option. +``` bash +# 3days, 12hours, 30minutes later from the midnight of today. +$ ut g -b today -d 3day -d 12hour -d 30minute +1561174200 + +# You can use short name on time unit. +$ ut g -b today -d 3d -d 12h -d 30min +1561174200 +``` + +#### Parse a unix timestamp + +Parse a unix timestamp and print it in human readable format. +``` bash +$ ut p $(ut g -b today) +2019-06-19 00:00:00 (+09:00) + +# You can parse timestamp in milliseconds. +$ ut p -p ms $(ut g -b today -p ms -d 11h -d 22min -d 33s -d 444ms) +2019-06-19 11:22:33.444 (+09:00) +``` + +### TODO +- Add more information on README +- CI/CD diff --git a/src/cmd.rs b/src/cmd.rs new file mode 100644 index 0000000..3f14f20 --- /dev/null +++ b/src/cmd.rs @@ -0,0 +1,2 @@ +pub mod generate; +pub mod parse; diff --git a/src/cmd/generate.rs b/src/cmd/generate.rs new file mode 100644 index 0000000..238795b --- /dev/null +++ b/src/cmd/generate.rs @@ -0,0 +1,164 @@ +use std::convert::TryFrom; + +use chrono::{Local, TimeZone, Utc}; +use clap::{App, Arg, ArgMatches, SubCommand}; +use regex::Regex; + +use timedelta::{ApplyDateTime, TimeDeltaBuilder}; + +use crate::error::{UtError, UtErrorKind}; + +mod request; +use request::Request; + +fn validate_number(field_name: &str, min: i32, max: i32, text: &str) -> Result<(), String> { + let number = text + .parse::() + .map_err(|_| format!("{} is not a number.", text))?; + + if number >= min && number <= max { + Ok(()) + } else { + Err(format!( + "{} must be between {} and {} . given {}: {}", + field_name, min, max, field_name, text + )) + } +} + +fn validate_ymd(ymd: String) -> Result<(), String> { + let re = Regex::new(r"(\d{4})(\d{2})(\d{2})").expect("wrong regex pattern"); + let caps = re.captures(&ymd).ok_or(format!( + "format must be \"yyyyMMdd\". given format: {}", + ymd + ))?; + + let y = caps.get(1).unwrap().as_str(); + validate_number("year", 1900, 2999, y)?; + + let m = caps.get(2).unwrap().as_str(); + validate_number("month", 1, 12, m)?; + + let d = caps.get(3).unwrap().as_str(); + validate_number("day", 1, 31, d)?; + + Ok(()) +} + +fn validate_hms(hms: String) -> Result<(), String> { + let re = Regex::new(r"(\d{2})(\d{2})(\d{2})").expect("wrong regex pattern"); + let caps = re + .captures(&hms) + .ok_or(format!("format must be \"HHmmss\". given format: {}", hms))?; + + let h = caps.get(1).unwrap().as_str(); + validate_number("hour", 0, 23, h)?; + + let m = caps.get(2).unwrap().as_str(); + validate_number("minute", 0, 59, m)?; + + let s = caps.get(3).unwrap().as_str(); + validate_number("second", 0, 59, s)?; + + Ok(()) +} + +pub fn command(name: &str) -> App<'static, 'static> { + SubCommand::with_name(name) + .about("Generate unix timestamp with given options.") + .arg( + Arg::with_name("BASE") + .value_name("DATE") + .help("Set base DATE from presets.") + .next_line_help(true) + .short("b") + .long("base") + .takes_value(true) + .conflicts_with("YMD"), + ) + .arg( + Arg::with_name("YMD") + .value_name("DATE") + .help("Set the DATE in yyyyMMdd format.") + .long("ymd") + .takes_value(true) + .validator(validate_ymd) + .conflicts_with("BASE"), + ) + .arg( + Arg::with_name("HMS") + .value_name("TIME") + .help("Set the TIME in HHmmss format.") + .long("hms") + .takes_value(true) + .validator(validate_hms), + ) + .arg( + Arg::with_name("TRUNCATE") + .value_name("UNIT") + .help("Set the UNIT to truncate the base DATE and TIME.") + .next_line_help(true) + .short("t") + .long("truncate") + .takes_value(true), + ) + .arg( + Arg::with_name("DELTA") + .help("Set the timedelta consists of VALUE and UNIT.") + .long_help( + " +Example: + --delta=3day : 3 days later. + -d 1y -d -10h : 10 hours ago in next year. +", + ) + .next_line_help(true) + // TODO: long helpに使用例を追加 + .short("d") + .long("delta") + .takes_value(true) + .allow_hyphen_values(true) + .multiple(true) + .number_of_values(1), + ) + .arg( + Arg::with_name("PRECISION") + .help("Set the precision of output timestamp.") + .next_line_help(true) + .short("p") + .long("precision") + .takes_value(true) + .default_value("second"), + ) + .arg( + Arg::with_name("UTC") + .help("Use utc date and time on given options relate to date and time.") + .short("u") + .long("utc"), + ) +} + +fn generate(request: Request) -> Result<(), UtError> { + let delta = request + .deltas() + .into_iter() + .fold(TimeDeltaBuilder::default(), |b, d| { + d.apply_timedelta_builder(b) + }) + .build(); + + match delta.apply_datetime(request.base()) { + Some(dt) => { + println!("{}", request.precision().to_timestamp(dt)); + Ok(()) + } + None => Err(UtError::from(UtErrorKind::TimeUnitError)), + } +} + +pub fn run(m: &ArgMatches<'static>) -> Result<(), UtError> { + match m.value_of("UTC") { + Some(_) => generate(Request::::try_from(m)?), + None => generate(Request::::try_from(m)?), + } +} diff --git a/src/cmd/generate/request.rs b/src/cmd/generate/request.rs new file mode 100644 index 0000000..41ebfa7 --- /dev/null +++ b/src/cmd/generate/request.rs @@ -0,0 +1,169 @@ +use std::convert::TryFrom; +use std::str::FromStr; + +use chrono::{Date, DateTime, Local, LocalResult, NaiveTime, TimeZone, Utc}; +use clap::{ArgMatches, Values}; +use failure::{Fail, ResultExt}; + +use crate::delta::DeltaItem; +use crate::error::{UtError, UtErrorKind}; +use crate::precision::Precision; +use crate::preset::{DateFixture, LocalDateFixture, Preset, UtcDateFixture}; +use crate::unit::TimeUnit; + +pub struct Request { + base: DateTime, + deltas: Vec, + precision: Precision, +} + +impl Request { + pub fn base(&self) -> DateTime { + self.base.clone() + } + + pub fn deltas(&self) -> &[DeltaItem] { + &self.deltas + } + + pub fn precision(&self) -> Precision { + self.precision + } +} + +impl TryFrom<&ArgMatches<'static>> for Request { + type Error = UtError; + + fn try_from(m: &ArgMatches) -> Result { + parse(m, UtcDateFixture {}) + } +} + +impl TryFrom<&ArgMatches<'static>> for Request { + type Error = UtError; + + fn try_from(m: &ArgMatches) -> Result { + parse(m, LocalDateFixture {}) + } +} + +fn parse(m: &ArgMatches, fixture: F) -> Result, UtError> +where + Tz: TimeZone, + F: DateFixture, +{ + let base = parse_base( + fixture, + m.value_of("BASE"), + m.value_of("YMD"), + m.value_of("HMS"), + m.value_of("TRUNCATE"), + )?; + let deltas = parse_deltas(m.values_of("DELTA"))?; + let precision = parse_precision(m.value_of("PRECISION"))?; + + Ok(Request { + base, + deltas, + precision, + }) +} + +fn extract_int(s: &str, start: usize, stop: usize) -> i32 { + *&s[start..stop].parse().expect("not a number") +} + +fn parse_ymd(tz: Tz, ymd: &str) -> Result, UtError> { + let year_len = ymd.len() - 4; + let y = extract_int(ymd, 0, year_len); + let m = extract_int(ymd, year_len, year_len + 2); + let d = extract_int(ymd, year_len + 2, year_len + 4); + + match tz.ymd_opt(y, m as u32, d as u32) { + LocalResult::Single(date) => Ok(date), + LocalResult::None => Err(UtError::from(UtErrorKind::WrongDate)), + LocalResult::Ambiguous(_, _) => Err(UtError::from(UtErrorKind::AmbiguousDate)), + } +} + +fn parse_hms(hms: &str) -> NaiveTime { + let h = *&hms[0..2].parse::().expect("not a number"); + let m = *&hms[2..4].parse::().expect("not a number"); + let s = *&hms[4..6].parse::().expect("not a number"); + + NaiveTime::from_hms(h, m, s) +} + +fn parse_base( + fixture: F, + maybe_base: Option<&str>, + maybe_ymd: Option<&str>, + maybe_hms: Option<&str>, + maybe_truncate: Option<&str>, +) -> Result, UtError> +where + F: DateFixture, + Tz: TimeZone, +{ + let now = fixture.now(); + + // date (preset => ymd) + let maybe_date = maybe_base + .map(|s| { + Preset::find_by_name(s) + .map(|p| p.as_date(&fixture)) + .context(UtErrorKind::PresetError) + .map_err(UtError::from) + }) + .or_else(|| maybe_ymd.map(|ymd| parse_ymd(fixture.timezone(), ymd))); + + // time (hms) + let maybe_time = maybe_hms.map(parse_hms); + + // datetime + let dt = maybe_date + .map(|date| { + date.map(|d| { + maybe_time + .map(|t| d.and_time(t).expect("not a datetime")) + .unwrap_or(d.and_hms(0, 0, 0)) + }) + }) + .unwrap_or_else(|| { + Ok(maybe_time + .map(|t| now.date().and_time(t).unwrap()) + .unwrap_or(now)) + })?; + + // truncate + Ok(if let Some(truncate) = maybe_truncate { + let truncate_unit = TimeUnit::find_by_name(truncate).context(UtErrorKind::TimeUnitError)?; + truncate_unit.truncate(dt) + } else { + dt + }) +} + +fn parse_deltas(maybe_values: Option) -> Result, UtError> { + maybe_values + .map(|values| { + values + .map(|v| { + DeltaItem::from_str(v) + .context(UtErrorKind::DeltaError) + .map_err(UtError::from) + }) + .collect() + }) + .unwrap_or(Ok(Vec::new())) +} + +fn parse_precision(maybe_precision: Option<&str>) -> Result { + maybe_precision + .map(|p| { + Precision::find_by_name(p) + .map_err(|e| e.context(UtErrorKind::PrecisionError)) + .map_err(UtError::from) + }) + .unwrap_or(Ok(Precision::Second)) +} diff --git a/src/cmd/parse.rs b/src/cmd/parse.rs new file mode 100644 index 0000000..5ae2fe0 --- /dev/null +++ b/src/cmd/parse.rs @@ -0,0 +1,50 @@ +use chrono::Local; +use clap::{App, Arg, ArgMatches, SubCommand}; +use failure::Fail; +use regex::Regex; + +use crate::error::{UtError, UtErrorKind}; +use crate::precision::Precision; + +pub fn command(name: &str) -> App<'static, 'static> { + SubCommand::with_name(name) + .about("Parse a unix timestamp and print it in human readable format.") + .arg( + Arg::with_name("TIMESTAMP") + .required(true) + .validator(is_timestamp), + ) + .arg( + // TODO: add validator + Arg::with_name("PRECISION") + .short("p") + .long_help("precision") + .takes_value(true) + .default_value("second"), + ) +} + +pub fn run(m: &ArgMatches<'static>) -> Result<(), UtError> { + let timestamp = m + .value_of("TIMESTAMP") + .unwrap() + .parse::() + .expect("not a number."); + + let precision = Precision::find_by_name(m.value_of("PRECISION").unwrap()) + .map_err(|e| e.context(UtErrorKind::PrecisionError)) + .map_err(UtError::from)?; + + let dt = precision.parse_timestamp(Local, timestamp); + println!("{}", dt.format(precision.preferred_format()).to_string()); + Ok(()) +} + +fn is_timestamp(s: String) -> Result<(), String> { + let re = Regex::new(r"[-+]?\d+").expect("wrong regex pattern."); + if re.is_match(&s) { + Ok(()) + } else { + Err(format!("TIMESTAMP must be a number. given: {}", s)) + } +} diff --git a/src/delta.rs b/src/delta.rs new file mode 100644 index 0000000..0dcb4ac --- /dev/null +++ b/src/delta.rs @@ -0,0 +1,119 @@ +use std::str::FromStr; + +use failure::Fail; +use regex::Regex; +use timedelta::TimeDeltaBuilder; + +use crate::find::FindError; +use crate::unit::TimeUnit; + +#[derive(Fail, Debug, PartialEq)] +pub enum DeltaItemError { + #[fail(display = "Wrong delta format: {}", _0)] + WrongFormat(String), + + #[fail(display = "Parse int error: {}", _0)] + ParseInt(String), + + #[fail(display = "TimeUnit find error: {}", _0)] + TimeUnitFindError(FindError), +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct DeltaItem { + unit: TimeUnit, + value: i32, +} + +impl DeltaItem { + #[cfg(test)] + pub fn new(unit: TimeUnit, value: i32) -> DeltaItem { + DeltaItem { unit, value } + } + + pub fn apply_timedelta_builder(&self, builder: TimeDeltaBuilder) -> TimeDeltaBuilder { + match self.unit { + TimeUnit::Year => builder.add_years(self.value), + TimeUnit::Month => builder.add_months(self.value), + TimeUnit::Day => builder.add_days(self.value), + TimeUnit::Hour => builder.add_hours(self.value), + TimeUnit::Minute => builder.add_minutes(self.value), + TimeUnit::Second => builder.add_seconds(self.value), + TimeUnit::MilliSecond => builder.add_milliseconds(self.value), + } + } +} + +impl FromStr for DeltaItem { + type Err = DeltaItemError; + + fn from_str(s: &str) -> Result { + let re = Regex::new(r"^([-+]?\d+)([a-zA-Z]+)$").expect("wrong regex pattern."); + let maybe_caps = re.captures(s); + + maybe_caps + .map(|caps| { + let r_value = caps + .get(1) + .unwrap() + .as_str() + .parse::() + .map_err(|e| DeltaItemError::ParseInt(e.to_string())); + + TimeUnit::find_by_name(caps.get(2).unwrap().as_str()) + .map_err(DeltaItemError::TimeUnitFindError) + .and_then(|unit| r_value.map(|value| DeltaItem { unit, value })) + }) + .unwrap_or(Err(DeltaItemError::WrongFormat(s.to_string()))) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::delta::{DeltaItem, DeltaItemError}; + use crate::find::FindError; + use crate::unit::TimeUnit; + + #[test] + fn delta_from_str() { + assert_eq!( + DeltaItem::from_str("12y"), + Ok(DeltaItem::new(TimeUnit::Year, 12)) + ); + assert_eq!( + DeltaItem::from_str("-10mon"), + Ok(DeltaItem::new(TimeUnit::Month, -10)) + ); + assert_eq!( + DeltaItem::from_str("+31d"), + Ok(DeltaItem::new(TimeUnit::Day, 31)) + ); + + assert_eq!( + DeltaItem::from_str("+ 31d"), + Err(DeltaItemError::WrongFormat("+ 31d".to_string())) + ); + + assert_eq!( + DeltaItem::from_str("aa d"), + Err(DeltaItemError::WrongFormat("aa d".to_string())) + ); + + assert!(DeltaItem::from_str("12345678901d") + .err() + .map(|e| match e { + DeltaItemError::ParseInt(_) => true, + _ => false, + }) + .unwrap_or(false)); + + assert_eq!( + DeltaItem::from_str("31b"), + Err(DeltaItemError::TimeUnitFindError(FindError::NotFound( + "b".to_string() + ))) + ); + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..084370f --- /dev/null +++ b/src/error.rs @@ -0,0 +1,62 @@ +use failure::{Backtrace, Context, Fail}; +use std::fmt::{Display, Formatter}; + +#[derive(Debug, Copy, Clone, PartialEq, Fail)] +pub enum UtErrorKind { + #[fail(display = "Time unit error.")] + TimeUnitError, + + #[fail(display = "Preset error.")] + PresetError, + + #[fail(display = "Delta error.")] + DeltaError, + + #[fail(display = "Precision error.")] + PrecisionError, + + #[fail(display = "Wrong date.")] + WrongDate, + + #[fail(display = "Date is ambiguous.")] + AmbiguousDate, +} + +#[derive(Debug)] +pub struct UtError { + inner: Context, +} + +impl Fail for UtError { + fn name(&self) -> Option<&str> { + self.inner.name() + } + + fn cause(&self) -> Option<&Fail> { + unimplemented!() + } + + fn backtrace(&self) -> Option<&Backtrace> { + unimplemented!() + } +} + +impl Display for UtError { + fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { + Display::fmt(&self.inner, f) + } +} + +impl From for UtError { + fn from(kind: UtErrorKind) -> Self { + UtError { + inner: Context::new(kind), + } + } +} + +impl From> for UtError { + fn from(inner: Context) -> Self { + UtError { inner } + } +} diff --git a/src/find.rs b/src/find.rs new file mode 100644 index 0000000..73ad099 --- /dev/null +++ b/src/find.rs @@ -0,0 +1,51 @@ +use std::fmt::Display; +use std::str::FromStr; + +use failure::Fail; +use strum::IntoEnumIterator; + +#[derive(Fail, Debug, PartialEq)] +pub enum FindError { + #[fail(display = "No matching item found. given: {}", _0)] + NotFound(String), + + #[fail( + display = "Multiple candidates found. given: {}, candidates: {:?}", + _0, _1 + )] + Ambiguous(String, Vec), +} + +pub fn find_items(items: I, name: &str) -> Vec +where + E: ToString + Copy, + I: Iterator, +{ + items.filter(|x| x.to_string().starts_with(name)).collect() +} + +pub fn find_enum_item(name: &str) -> Result +where + E: IntoEnumIterator + FromStr + Copy + Display, + I: Iterator, +{ + E::from_str(name).map(|x| Ok(x)).unwrap_or_else(|_| { + let items = find_items(E::iter(), name); + if items.len() == 1 { + Ok(*items.first().unwrap()) + } else if items.is_empty() { + Err(FindError::NotFound(name.to_string())) + } else { + let names = items.into_iter().map(|x| x.to_string()).collect(); + Err(FindError::Ambiguous(name.to_string(), names)) + } + }) +} + +pub fn enum_names(items: I) -> Vec +where + E: ToString, + I: Iterator, +{ + items.map(|x| x.to_string()).collect() +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c662e29 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,38 @@ +mod cmd; +mod delta; +mod error; +mod find; +mod precision; +mod preset; +mod unit; + +use clap::{crate_authors, crate_description, crate_name, crate_version, App, AppSettings}; + +use error::UtError; + +fn app() -> App<'static, 'static> { + App::new(crate_name!()) + .author(crate_authors!()) + .version(crate_version!()) + .about(crate_description!()) + .settings(&[AppSettings::SubcommandRequiredElseHelp]) + .subcommand(cmd::generate::command("generate").alias("g")) + .subcommand(cmd::parse::command("parse").alias("p")) +} + +fn run() -> Result<(), UtError> { + let app = app(); + let main_matches = app.get_matches(); + match main_matches.subcommand() { + ("generate", generate_matches) => cmd::generate::run(generate_matches.unwrap()), + ("parse", parse_matches) => cmd::parse::run(parse_matches.unwrap()), + _ => panic!("never happen"), + } +} + +fn main() { + match run() { + Ok(_) => (), + Err(e) => eprintln!("error: {}", e), + } +} diff --git a/src/precision.rs b/src/precision.rs new file mode 100644 index 0000000..4a0de38 --- /dev/null +++ b/src/precision.rs @@ -0,0 +1,111 @@ +use chrono::{DateTime, TimeZone}; +use lazy_static::lazy_static; +use strum::IntoEnumIterator; +use strum_macros::{Display, EnumIter, EnumString}; + +use crate::find::{enum_names, find_enum_item, FindError}; + +lazy_static! { + static ref PRESET_NAMES: Vec = enum_names(Precision::iter()); + static ref POSSIBLE_VALUES: Vec<&'static str> = + PRESET_NAMES.iter().map(|s| s.as_str()).collect(); +} + +#[derive(Debug, Copy, Clone, PartialEq, EnumIter, EnumString, Display)] +pub enum Precision { + #[strum(serialize = "second")] + Second, + + #[strum(serialize = "millisecond", serialize = "ms")] + MilliSecond, +} + +impl Precision { + pub fn find_by_name(name: &str) -> Result { + find_enum_item(&name.to_ascii_lowercase()) + } + + pub fn parse_timestamp(&self, tz: Tz, timestamp: i64) -> DateTime { + match *self { + Precision::Second => tz.timestamp(timestamp, 0), + Precision::MilliSecond => tz.timestamp_millis(timestamp), + } + } + + pub fn to_timestamp(&self, dt: DateTime) -> i64 { + match *self { + Precision::Second => dt.timestamp(), + Precision::MilliSecond => dt.timestamp_millis(), + } + } + + pub fn preferred_format(&self) -> &'static str { + match *self { + Precision::Second => "%Y-%m-%d %H:%M:%S (%Z)", + Precision::MilliSecond => "%Y-%m-%d %H:%M:%S%.3f (%Z)", + } + } +} + +#[cfg(test)] +mod tests { + use chrono::offset::TimeZone; + use chrono::Utc; + + use crate::find::FindError; + use crate::precision::Precision; + + #[test] + fn find_by_name_second() { + assert_eq!(Precision::find_by_name("second"), Ok(Precision::Second)); + assert_eq!(Precision::find_by_name("s"), Ok(Precision::Second)); + } + + #[test] + fn find_by_name_millisecond() { + assert_eq!( + Precision::find_by_name("millisecond"), + Ok(Precision::MilliSecond) + ); + assert_eq!(Precision::find_by_name("m"), Ok(Precision::MilliSecond)); + assert_eq!(Precision::find_by_name("ms"), Ok(Precision::MilliSecond)); + } + + #[test] + fn find_by_name_not_supported() { + assert_eq!( + Precision::find_by_name("year"), + Err(FindError::NotFound("year".to_string())) + ); + assert_eq!( + Precision::find_by_name("min"), + Err(FindError::NotFound("min".to_string())) + ); + } + + #[test] + fn parse_timestamp_second() { + assert_eq!( + Precision::Second.parse_timestamp(Utc, 0), + Utc.ymd(1970, 1, 1).and_hms(0, 0, 0) + ); + + assert_eq!( + Precision::Second.parse_timestamp(Utc, 1560762129123), + Utc.ymd(51428, 8, 1).and_hms(11, 52, 3) + ); + } + + #[test] + fn parse_timestamp_millisecond() { + assert_eq!( + Precision::MilliSecond.parse_timestamp(Utc, 0), + Utc.ymd(1970, 1, 1).and_hms_milli(0, 0, 0, 0) + ); + + assert_eq!( + Precision::MilliSecond.parse_timestamp(Utc, 1560762129123), + Utc.ymd(2019, 6, 17).and_hms_milli(9, 2, 9, 123) + ); + } +} diff --git a/src/preset.rs b/src/preset.rs new file mode 100644 index 0000000..7f46903 --- /dev/null +++ b/src/preset.rs @@ -0,0 +1,100 @@ +use chrono::{Date, DateTime, Local, TimeZone, Utc}; +use lazy_static::lazy_static; +use strum::IntoEnumIterator; +use strum_macros::{Display, EnumIter, EnumString}; + +use timedelta::{ApplyDateTime, TimeDeltaBuilder}; + +use crate::find::{enum_names, find_enum_item, FindError}; + +#[derive(Debug, Copy, Clone, PartialEq, EnumIter, EnumString, Display)] +pub enum Preset { + #[strum(serialize = "today")] + Today, + + #[strum(serialize = "tomorrow")] + Tomorrow, + + #[strum(serialize = "yesterday")] + Yesterday, +} + +lazy_static! { + static ref PRESET_NAMES: Vec = enum_names(Preset::iter()); + static ref POSSIBLE_VALUES: Vec<&'static str> = + PRESET_NAMES.iter().map(|s| s.as_str()).collect(); +} + +impl Preset { + pub fn find_by_name(name: &str) -> Result { + find_enum_item(&name.to_ascii_lowercase()) + } + + pub fn as_date(&self, fixture: &F) -> Date + where + F: DateFixture, + Tz: TimeZone, + { + match *self { + Preset::Today => fixture.today(), + Preset::Tomorrow => fixture.tomorrow(), + Preset::Yesterday => fixture.yesterday(), + } + } +} + +fn add_days(date: Date, days: i32) -> Date { + let delta = TimeDeltaBuilder::default().days(days).build(); + delta + .apply_datetime(date.and_hms(0, 0, 0)) + .expect(&format!("can't add days. date={:?}, days={}", date, days)) + .date() +} + +pub trait DateFixture { + fn timezone(&self) -> Tz; + + fn now(&self) -> DateTime; + + fn today(&self) -> Date; + + fn tomorrow(&self) -> Date { + add_days(self.today(), 1) + } + + fn yesterday(&self) -> Date { + add_days(self.today(), -1) + } +} + +pub struct UtcDateFixture {} + +impl DateFixture for UtcDateFixture { + fn timezone(&self) -> Utc { + Utc + } + + fn now(&self) -> DateTime { + Utc::now() + } + + fn today(&self) -> Date { + Utc::today() + } +} + +pub struct LocalDateFixture {} + +impl DateFixture for LocalDateFixture { + fn timezone(&self) -> Local { + Local + } + + fn now(&self) -> DateTime { + Local::now() + } + + fn today(&self) -> Date { + Local::today() + } +} diff --git a/src/unit.rs b/src/unit.rs new file mode 100644 index 0000000..c7a5920 --- /dev/null +++ b/src/unit.rs @@ -0,0 +1,247 @@ +use chrono::{DateTime, Datelike, TimeZone, Timelike}; +use lazy_static::lazy_static; +use strum::IntoEnumIterator; +use strum_macros::{Display, EnumIter, EnumString}; + +use crate::find::{enum_names, find_enum_item, FindError}; + +lazy_static! { + static ref PRESET_NAMES: Vec = enum_names(TimeUnit::iter()); + static ref POSSIBLE_VALUES: Vec<&'static str> = + PRESET_NAMES.iter().map(|s| s.as_str()).collect(); +} + +#[derive(Debug, Copy, Clone, PartialEq, EnumIter, EnumString, Display)] +pub enum TimeUnit { + #[strum(serialize = "year")] + Year, + + #[strum(serialize = "month")] + Month, + + #[strum(serialize = "day")] + Day, + + #[strum(serialize = "hour")] + Hour, + + #[strum(serialize = "minute")] + Minute, + + #[strum(serialize = "second")] + Second, + + #[strum(serialize = "millisecond", serialize = "ms")] + MilliSecond, +} + +impl TimeUnit { + pub fn find_by_name(name: &str) -> Result { + find_enum_item(&name.to_ascii_lowercase()) + } + + pub fn truncate(&self, dt: DateTime) -> DateTime { + let d = match *self { + TimeUnit::Year => dt.date().with_month(1).unwrap().with_day(1).unwrap(), + TimeUnit::Month => dt.date().with_day(1).unwrap(), + _ => dt.date(), + }; + + match *self { + TimeUnit::Hour => d.and_hms(dt.hour(), 0, 0), + TimeUnit::Minute => d.and_hms(dt.hour(), dt.minute(), 0), + TimeUnit::Second => d.and_hms(dt.hour(), dt.minute(), dt.second()), + TimeUnit::MilliSecond => d.and_hms_milli( + dt.hour(), + dt.minute(), + dt.second(), + dt.timestamp_subsec_millis(), + ), + _ => d.and_hms(0, 0, 0), + } + } +} + +#[cfg(test)] +mod find_tests { + use crate::find::FindError; + use crate::unit::TimeUnit; + + #[test] + fn find_by_name_year() { + assert_eq!(TimeUnit::find_by_name("year"), Ok(TimeUnit::Year)); + assert_eq!(TimeUnit::find_by_name("YEAR"), Ok(TimeUnit::Year)); + assert_eq!(TimeUnit::find_by_name("y"), Ok(TimeUnit::Year)); + } + + #[test] + fn find_by_name_month() { + assert_eq!(TimeUnit::find_by_name("month"), Ok(TimeUnit::Month)); + assert_eq!(TimeUnit::find_by_name("mo"), Ok(TimeUnit::Month)); + + assert_eq!( + TimeUnit::find_by_name("m"), + Err(FindError::Ambiguous( + "m".to_string(), + vec![ + "month".to_string(), + "minute".to_string(), + "millisecond".to_string() + ] + )) + ); + } + + #[test] + fn find_by_name_day() { + assert_eq!(TimeUnit::find_by_name("day"), Ok(TimeUnit::Day)); + assert_eq!(TimeUnit::find_by_name("d"), Ok(TimeUnit::Day)); + } + + #[test] + fn find_by_name_hour() { + assert_eq!(TimeUnit::find_by_name("hour"), Ok(TimeUnit::Hour)); + assert_eq!(TimeUnit::find_by_name("h"), Ok(TimeUnit::Hour)); + } + + #[test] + fn find_by_name_minute() { + assert_eq!(TimeUnit::find_by_name("minute"), Ok(TimeUnit::Minute)); + assert_eq!(TimeUnit::find_by_name("min"), Ok(TimeUnit::Minute)); + + assert_eq!( + TimeUnit::find_by_name("mi"), + Err(FindError::Ambiguous( + "mi".to_string(), + vec!["minute".to_string(), "millisecond".to_string()] + )) + ); + } + + #[test] + fn find_by_name_second() { + assert_eq!(TimeUnit::find_by_name("second"), Ok(TimeUnit::Second)); + assert_eq!(TimeUnit::find_by_name("s"), Ok(TimeUnit::Second)); + } + + #[test] + fn find_by_name_milli_second() { + assert_eq!( + TimeUnit::find_by_name("millisecond"), + Ok(TimeUnit::MilliSecond) + ); + assert_eq!(TimeUnit::find_by_name("mil"), Ok(TimeUnit::MilliSecond)); + assert_eq!(TimeUnit::find_by_name("ms"), Ok(TimeUnit::MilliSecond)); + } + + #[test] + fn find_by_name_not_supported() { + assert_eq!( + TimeUnit::find_by_name("b"), + Err(FindError::NotFound("b".to_string())) + ); + } +} + +#[cfg(test)] +mod truncate_tests { + use crate::unit::TimeUnit; + + use chrono::offset::TimeZone; + use chrono::{DateTime, Utc}; + + fn base_date() -> DateTime { + Utc.ymd(2019, 6, 17).and_hms_milli(11, 22, 33, 444) + } + + #[test] + fn truncate_year() { + assert_eq!( + TimeUnit::Year.truncate(base_date()), + Utc.ymd(2019, 1, 1).and_hms(0, 0, 0) + ); + + assert_eq!( + TimeUnit::Year.truncate(Utc.ymd(2019, 1, 1).and_hms(0, 0, 0)), + Utc.ymd(2019, 1, 1).and_hms(0, 0, 0) + ); + } + + #[test] + fn truncate_month() { + assert_eq!( + TimeUnit::Month.truncate(base_date()), + Utc.ymd(2019, 6, 1).and_hms(0, 0, 0) + ); + + assert_eq!( + TimeUnit::Month.truncate(Utc.ymd(2019, 6, 1).and_hms(0, 0, 0)), + Utc.ymd(2019, 6, 1).and_hms(0, 0, 0) + ); + } + + #[test] + fn truncate_day() { + assert_eq!( + TimeUnit::Day.truncate(base_date()), + Utc.ymd(2019, 6, 17).and_hms(0, 0, 0) + ); + + assert_eq!( + TimeUnit::Day.truncate(Utc.ymd(2019, 6, 17).and_hms(0, 0, 0)), + Utc.ymd(2019, 6, 17).and_hms(0, 0, 0) + ); + } + + #[test] + fn truncate_hour() { + assert_eq!( + TimeUnit::Hour.truncate(base_date()), + Utc.ymd(2019, 6, 17).and_hms(11, 0, 0) + ); + + assert_eq!( + TimeUnit::Hour.truncate(Utc.ymd(2019, 6, 17).and_hms(11, 0, 0)), + Utc.ymd(2019, 6, 17).and_hms(11, 0, 0) + ); + } + + #[test] + fn truncate_minute() { + assert_eq!( + TimeUnit::Minute.truncate(base_date()), + Utc.ymd(2019, 6, 17).and_hms(11, 22, 0) + ); + + assert_eq!( + TimeUnit::Minute.truncate(Utc.ymd(2019, 6, 17).and_hms(11, 22, 0)), + Utc.ymd(2019, 6, 17).and_hms(11, 22, 0) + ); + } + + #[test] + fn truncate_second() { + assert_eq!( + TimeUnit::Second.truncate(base_date()), + Utc.ymd(2019, 6, 17).and_hms(11, 22, 33) + ); + + assert_eq!( + TimeUnit::Second.truncate(Utc.ymd(2019, 6, 17).and_hms(11, 22, 33)), + Utc.ymd(2019, 6, 17).and_hms(11, 22, 33) + ); + } + + #[test] + fn truncate_millisecond() { + assert_eq!( + TimeUnit::MilliSecond.truncate(base_date()), + Utc.ymd(2019, 6, 17).and_hms_micro(11, 22, 33, 444_000) + ); + + assert_eq!( + TimeUnit::MilliSecond.truncate(Utc.ymd(2019, 6, 17).and_hms_milli(11, 22, 33, 444)), + Utc.ymd(2019, 6, 17).and_hms_milli(11, 22, 33, 444) + ); + } +} diff --git a/timedelta/Cargo.toml b/timedelta/Cargo.toml new file mode 100644 index 0000000..2ef1431 --- /dev/null +++ b/timedelta/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "timedelta" +version = "0.1.0" +authors = ["yoshihitoh "] +edition = "2018" + +[dependencies] +chrono = "^0.4" +time = "^0.1" diff --git a/timedelta/src/delta.rs b/timedelta/src/delta.rs new file mode 100644 index 0000000..1bbed60 --- /dev/null +++ b/timedelta/src/delta.rs @@ -0,0 +1,799 @@ +use chrono::{DateTime, Datelike, TimeZone}; +use time::Duration; + +pub trait ApplyDateTime { + fn apply_datetime(&self, dt: DateTime) -> Option>; +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct TimeDelta { + values: DeltaValues, +} + +impl TimeDelta { + pub fn new( + years: i32, + months: i32, + days: i32, + hours: i32, + minutes: i32, + seconds: i32, + microseconds: i32, + ) -> Self { + // microseconds + let sign = sign_of(microseconds); + let (d, m) = div_mod(microseconds * sign, 1_000_000); + let seconds = seconds + d * sign; + let microseconds = m * sign; + + // seconds + let sign = sign_of(seconds); + let (d, m) = div_mod(seconds * sign, 60); + let minutes = minutes + d * sign; + let seconds = m * sign; + + // minutes + let sign = sign_of(minutes); + let (d, m) = div_mod(minutes * sign, 60); + let hours = hours + d * sign; + let minutes = m * sign; + + // hours + let sign = sign_of(hours); + let (d, m) = div_mod(hours * sign, 24); + let days = days + d * sign; + let hours = m * sign; + + // NOTE: cannot convert days to months. + + // months + let sign = sign_of(months); + let (d, m) = div_mod(months * sign, 12); + let years = years + d * sign; + let months = m * sign; + + TimeDelta { + values: DeltaValues { + years, + months, + days, + hours, + minutes, + seconds, + microseconds, + }, + } + } + + pub fn years(&self) -> i32 { + self.values.years + } + + pub fn months(&self) -> i32 { + self.values.months + } + + pub fn days(&self) -> i32 { + self.values.days + } + + pub fn hours(&self) -> i32 { + self.values.hours + } + + pub fn minutes(&self) -> i32 { + self.values.minutes + } + + pub fn seconds(&self) -> i32 { + self.values.seconds + } + + pub fn microseconds(&self) -> i32 { + self.values.microseconds + } +} + +impl ApplyDateTime for TimeDelta { + fn apply_datetime(&self, target: DateTime) -> Option> { + let duration = Duration::microseconds(self.microseconds() as i64) + + Duration::seconds(self.seconds() as i64) + + Duration::minutes(self.minutes() as i64) + + Duration::hours(self.hours() as i64) + + Duration::days(self.days() as i64); + + let duration_applied: DateTime = target + duration; + + let delta_months = self.years() * 12 + self.months(); + let sum_months = duration_applied.month() as i32 + delta_months; + + let delta_years = if sum_months > 0 { + (sum_months - 1) / 12 + } else { + (sum_months / 12) - 1 + }; + let result_year = duration_applied.year() + delta_years; + + let result_month = if sum_months > 0 { + (((sum_months - 1) % 12) + 1) + } else { + (sum_months % 12) + 12 + } as u32; + + duration_applied + .with_year(result_year) + .and_then(|dt| dt.with_month(result_month)) + } +} + +pub struct TimeDeltaBuilder { + values: DeltaValues, +} + +impl Default for TimeDeltaBuilder { + fn default() -> Self { + TimeDeltaBuilder { + values: DeltaValues { + years: 0, + months: 0, + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + microseconds: 0, + }, + } + } +} + +impl TimeDeltaBuilder { + pub fn years(mut self, value: i32) -> Self { + self.values.years = value; + self + } + + pub fn add_years(self, value: i32) -> Self { + let y = self.values.years + value; + self.years(y) + } + + pub fn months(mut self, value: i32) -> Self { + self.values.months = value; + self + } + + pub fn add_months(self, value: i32) -> Self { + let m = self.values.months + value; + self.months(m) + } + + pub fn days(mut self, d: i32) -> Self { + self.values.days = d; + self + } + + pub fn add_days(self, value: i32) -> Self { + let d = self.values.days + value; + self.days(d) + } + + pub fn hours(mut self, h: i32) -> Self { + self.values.hours = h; + self + } + + pub fn add_hours(self, value: i32) -> Self { + let h = self.values.hours + value; + self.hours(h) + } + + pub fn minutes(mut self, m: i32) -> Self { + self.values.minutes = m; + self + } + + pub fn add_minutes(self, value: i32) -> Self { + let m = self.values.minutes + value; + self.minutes(m) + } + + pub fn seconds(mut self, s: i32) -> Self { + self.values.seconds = s; + self + } + + pub fn add_seconds(self, value: i32) -> Self { + let s = self.values.seconds + value; + self.seconds(s) + } + + pub fn milliseconds(self, value: i32) -> Self { + let s = value / 1000; + let us = (value % 1000) * 1000; + self.seconds(s).microseconds(us) + } + + pub fn add_milliseconds(self, value: i32) -> Self { + let s = value / 1000; + let us = (value % 1000) * 1000; + + self.add_seconds(s).add_microseconds(us) + } + + pub fn microseconds(mut self, value: i32) -> Self { + self.values.microseconds = value; + self + } + + pub fn add_microseconds(self, value: i32) -> Self { + let us = self.values.microseconds + value; + self.microseconds(us) + } + + pub fn build(self) -> TimeDelta { + TimeDelta { + values: self.values, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +struct DeltaValues { + years: i32, + months: i32, + days: i32, + hours: i32, + minutes: i32, + seconds: i32, + microseconds: i32, +} + +fn sign_of(x: i32) -> i32 { + if x > 0 { + 1 + } else { + -1 + } +} + +fn div_mod(x: i32, y: i32) -> (i32, i32) { + (x / y, x % y) +} + +#[cfg(test)] +mod time_delta_tests { + use crate::delta::{ApplyDateTime, TimeDelta}; + use crate::TimeDeltaBuilder; + use chrono::offset::TimeZone; + use chrono::Utc; + + #[test] + fn time_delta_new_basics() { + let delta = TimeDelta::new(0, 0, 0, 0, 0, 0, 0); + assert_eq!(delta.years(), 0); + assert_eq!(delta.months(), 0); + assert_eq!(delta.days(), 0); + assert_eq!(delta.hours(), 0); + assert_eq!(delta.minutes(), 0); + assert_eq!(delta.seconds(), 0); + assert_eq!(delta.microseconds(), 0); + + let delta = TimeDelta::new(1234, 11, 365, 23, 59, 59, 999_999); + assert_eq!(delta.years(), 1234); + assert_eq!(delta.months(), 11); + assert_eq!(delta.days(), 365); + assert_eq!(delta.hours(), 23); + assert_eq!(delta.minutes(), 59); + assert_eq!(delta.seconds(), 59); + assert_eq!(delta.microseconds(), 999_999); + + let delta = TimeDelta::new(1234, 11, 365, 23, 59, 59, 1_000_000); + assert_eq!(delta.years(), 1234); + assert_eq!(delta.months(), 11); + assert_eq!(delta.days(), 366); + assert_eq!(delta.hours(), 0); + assert_eq!(delta.minutes(), 0); + assert_eq!(delta.seconds(), 0); + assert_eq!(delta.microseconds(), 0); + + let delta = TimeDelta::new(-1234, -11, -365, -23, -59, -59, -999_999); + assert_eq!(delta.years(), -1234); + assert_eq!(delta.months(), -11); + assert_eq!(delta.days(), -365); + assert_eq!(delta.hours(), -23); + assert_eq!(delta.minutes(), -59); + assert_eq!(delta.seconds(), -59); + assert_eq!(delta.microseconds(), -999_999); + + let delta = TimeDelta::new(-1234, -11, -365, -23, -59, -59, -1_000_000); + assert_eq!(delta.years(), -1234); + assert_eq!(delta.months(), -11); + assert_eq!(delta.days(), -366); + assert_eq!(delta.hours(), 0); + assert_eq!(delta.minutes(), 0); + assert_eq!(delta.seconds(), 0); + assert_eq!(delta.microseconds(), 0); + } + + #[test] + fn time_delta_new_microseconds() { + // plus + let delta = TimeDelta::new(0, 0, 0, 0, 0, 0, 999_999); + assert_eq!(delta.seconds(), 0); + assert_eq!(delta.microseconds(), 999_999); + + let delta = TimeDelta::new(0, 0, 0, 0, 0, 0, 1_000_000); + assert_eq!(delta.seconds(), 1); + assert_eq!(delta.microseconds(), 0); + + // minus + let delta = TimeDelta::new(0, 0, 0, 0, 0, 0, -999_999); + assert_eq!(delta.seconds(), 0); + assert_eq!(delta.microseconds(), -999_999); + + let delta = TimeDelta::new(0, 0, 0, 0, 0, 0, -1_000_000); + assert_eq!(delta.seconds(), -1); + assert_eq!(delta.microseconds(), 0); + } + + #[test] + fn time_delta_new_seconds() { + // plus + let delta = TimeDelta::new(0, 0, 0, 0, 0, 59, 0); + assert_eq!(delta.minutes(), 0); + assert_eq!(delta.seconds(), 59); + + let delta = TimeDelta::new(0, 0, 0, 0, 0, 60, 0); + assert_eq!(delta.minutes(), 1); + assert_eq!(delta.seconds(), 0); + + // minus + let delta = TimeDelta::new(0, 0, 0, 0, 0, -59, 0); + assert_eq!(delta.minutes(), 0); + assert_eq!(delta.seconds(), -59); + + let delta = TimeDelta::new(0, 0, 0, 0, 0, -60, 0); + assert_eq!(delta.minutes(), -1); + assert_eq!(delta.seconds(), 0); + } + + #[test] + fn time_delta_new_minutes() { + // minutes + let delta = TimeDelta::new(0, 0, 0, 0, 59, 0, 0); + assert_eq!(delta.hours(), 0); + assert_eq!(delta.minutes(), 59); + + let delta = TimeDelta::new(0, 0, 0, 1, 0, 0, 0); + assert_eq!(delta.hours(), 1); + assert_eq!(delta.minutes(), 0); + + // minutes + let delta = TimeDelta::new(0, 0, 0, 0, -59, 0, 0); + assert_eq!(delta.hours(), 0); + assert_eq!(delta.minutes(), -59); + + let delta = TimeDelta::new(0, 0, 0, 0, -60, 0, 0); + assert_eq!(delta.hours(), -1); + assert_eq!(delta.minutes(), 0); + } + + #[test] + fn time_delta_new_hours() { + // plus + let delta = TimeDelta::new(0, 0, 0, 23, 0, 0, 0); + assert_eq!(delta.days(), 0); + assert_eq!(delta.hours(), 23); + + let delta = TimeDelta::new(0, 0, 1, 0, 0, 0, 0); + assert_eq!(delta.days(), 1); + assert_eq!(delta.hours(), 0); + + // minus + let delta = TimeDelta::new(0, 0, 0, -23, 0, 0, 0); + assert_eq!(delta.days(), 0); + assert_eq!(delta.hours(), -23); + + let delta = TimeDelta::new(0, 0, 0, -24, 0, 0, 0); + assert_eq!(delta.days(), -1); + assert_eq!(delta.hours(), 0); + } + + #[test] + fn time_delta_new_days() { + // plus + let delta = TimeDelta::new(0, 0, 364, 0, 0, 0, 0); + assert_eq!(delta.months(), 0); + assert_eq!(delta.days(), 364); + + let delta = TimeDelta::new(0, 0, 365, 0, 0, 0, 0); + assert_eq!(delta.months(), 0); // NOTE: cannot calculate months from days. + assert_eq!(delta.days(), 365); + + // minus + let delta = TimeDelta::new(0, 0, -364, 0, 0, 0, 0); + assert_eq!(delta.months(), 0); + assert_eq!(delta.days(), -364); + + let delta = TimeDelta::new(0, 0, -365, 0, 0, 0, 0); + assert_eq!(delta.months(), 0); // NOTE: cannot calculate months from days. + assert_eq!(delta.days(), -365); + } + + #[test] + fn time_delta_new_months() { + // plus + let delta = TimeDelta::new(0, 11, 0, 0, 0, 0, 0); + assert_eq!(delta.years(), 0); + assert_eq!(delta.months(), 11); + + let delta = TimeDelta::new(0, 12, 0, 0, 0, 0, 0); + assert_eq!(delta.years(), 1); + assert_eq!(delta.months(), 0); + + // minus + let delta = TimeDelta::new(0, -11, 0, 0, 0, 0, 0); + assert_eq!(delta.years(), 0); + assert_eq!(delta.months(), -11); + + let delta = TimeDelta::new(0, -12, 0, 0, 0, 0, 0); + assert_eq!(delta.years(), -1); + assert_eq!(delta.months(), 0); + } + + #[test] + fn time_delta_new_years() { + // plus + let delta = TimeDelta::new(1, 0, 0, 0, 0, 0, 0); + assert_eq!(delta.years(), 1); + + // minus + let delta = TimeDelta::new(-1, 0, 0, 0, 0, 0, 0); + assert_eq!(delta.years(), -1); + } + + #[test] + fn time_delta_apply_microseconds() { + let date = Utc.ymd(1, 1, 1); + + // plus + assert_eq!( + TimeDeltaBuilder::default() + .microseconds(111_222) + .build() + .apply_datetime(date.and_hms_micro(0, 0, 0, 12_234)), + Some(date.and_hms_micro(0, 0, 0, 123_456)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .microseconds(999_999) + .build() + .apply_datetime(date.and_hms_micro(0, 0, 0, 1)), + Some(date.and_hms_micro(0, 0, 1, 0)) + ); + + // minus + assert_eq!( + TimeDeltaBuilder::default() + .microseconds(-1) + .build() + .apply_datetime(date.and_hms_micro(0, 0, 0, 1)), + Some(date.and_hms_micro(0, 0, 0, 0)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .microseconds(-1) + .build() + .apply_datetime(date.and_hms_micro(0, 0, 0, 0)), + Some(Utc.ymd(0, 12, 31).and_hms_micro(23, 59, 59, 999_999)) + ); + + let date = Utc.ymd(0, 1, 1); + assert_eq!( + TimeDeltaBuilder::default() + .microseconds(-1) + .build() + .apply_datetime(date.and_hms_micro(0, 0, 0, 0)), + Some(Utc.ymd(-1, 12, 31).and_hms_micro(23, 59, 59, 999_999)) + ); + } + + #[test] + fn time_delta_apply_seconds() { + let date = Utc.ymd(2019, 6, 12); + + // plus + assert_eq!( + TimeDeltaBuilder::default() + .seconds(1) + .build() + .apply_datetime(date.and_hms(0, 0, 58)), + Some(date.and_hms(0, 0, 59)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .seconds(2) + .build() + .apply_datetime(date.and_hms(0, 0, 58)), + Some(date.and_hms(0, 1, 0)) + ); + + // minus + assert_eq!( + TimeDeltaBuilder::default() + .seconds(-1) + .build() + .apply_datetime(date.and_hms(0, 0, 1)), + Some(date.and_hms(0, 0, 0)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .seconds(-1) + .build() + .apply_datetime(date.and_hms(0, 0, 0)), + Some(Utc.ymd(2019, 6, 11).and_hms(23, 59, 59)) + ); + } + + #[test] + fn time_delta_apply_minutes() { + let date = Utc.ymd(2019, 6, 12); + + // plus + assert_eq!( + TimeDeltaBuilder::default() + .minutes(1) + .build() + .apply_datetime(date.and_hms(0, 58, 0)), + Some(date.and_hms(0, 59, 0)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .minutes(1) + .build() + .apply_datetime(date.and_hms(0, 59, 0)), + Some(date.and_hms(1, 0, 0)) + ); + + // minus + assert_eq!( + TimeDeltaBuilder::default() + .minutes(-1) + .build() + .apply_datetime(date.and_hms(0, 1, 0)), + Some(date.and_hms(0, 0, 0)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .minutes(-2) + .build() + .apply_datetime(date.and_hms(0, 1, 0)), + Some(Utc.ymd(2019, 6, 11).and_hms(23, 59, 0)) + ); + } + + #[test] + fn time_delta_apply_hours() { + let date = Utc.ymd(2019, 6, 12); + + // plus + assert_eq!( + TimeDeltaBuilder::default() + .hours(1) + .build() + .apply_datetime(date.and_hms(22, 0, 0)), + Some(date.and_hms(23, 0, 0)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .hours(2) + .build() + .apply_datetime(date.and_hms(22, 0, 0)), + Some(Utc.ymd(2019, 6, 13).and_hms(0, 0, 0)) + ); + + // minus + assert_eq!( + TimeDeltaBuilder::default() + .hours(-1) + .build() + .apply_datetime(date.and_hms(1, 0, 0)), + Some(date.and_hms(0, 0, 0)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .hours(-2) + .build() + .apply_datetime(date.and_hms(1, 0, 0)), + Some(Utc.ymd(2019, 6, 11).and_hms(23, 0, 0)) + ); + } + + #[test] + fn time_delta_apply_days() { + // plus + assert_eq!( + TimeDeltaBuilder::default() + .days(28) + .build() + .apply_datetime(Utc.ymd(2019, 6, 2).and_hms(0, 0, 0)), + Some(Utc.ymd(2019, 6, 30).and_hms(0, 0, 0)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .days(29) + .build() + .apply_datetime(Utc.ymd(2019, 6, 2).and_hms(0, 0, 0)), + Some(Utc.ymd(2019, 7, 1).and_hms(0, 0, 0)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .days(28) + .build() + .apply_datetime(Utc.ymd(2019, 2, 1).and_hms(0, 0, 0)), + Some(Utc.ymd(2019, 3, 1).and_hms(0, 0, 0)) + ); + + // minus + assert_eq!( + TimeDeltaBuilder::default() + .days(-1) + .build() + .apply_datetime(Utc.ymd(2019, 6, 2).and_hms(0, 0, 0)), + Some(Utc.ymd(2019, 6, 1).and_hms(0, 0, 0)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .days(-2) + .build() + .apply_datetime(Utc.ymd(2019, 6, 2).and_hms(0, 0, 0)), + Some(Utc.ymd(2019, 5, 31).and_hms(0, 0, 0)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .days(-1) + .build() + .apply_datetime(Utc.ymd(2019, 3, 1).and_hms(0, 0, 0)), + Some(Utc.ymd(2019, 2, 28).and_hms(0, 0, 0)) + ); + } + + #[test] + fn time_delta_apply_months() { + // plus + assert_eq!( + TimeDeltaBuilder::default() + .months(1) + .build() + .apply_datetime(Utc.ymd(2019, 11, 1).and_hms(0, 0, 0)), + Some(Utc.ymd(2019, 12, 1).and_hms(0, 0, 0)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .months(2) + .build() + .apply_datetime(Utc.ymd(2019, 11, 1).and_hms(0, 0, 0)), + Some(Utc.ymd(2020, 1, 1).and_hms(0, 0, 0)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .months(2) + .build() + .apply_datetime(Utc.ymd(2019, 10, 31).and_hms(0, 0, 0)), + Some(Utc.ymd(2019, 12, 31).and_hms(0, 0, 0)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .months(1) + .build() + .apply_datetime(Utc.ymd(2019, 10, 31).and_hms(0, 0, 0)), + None + ); + + // minus + assert_eq!( + TimeDeltaBuilder::default() + .months(-1) + .build() + .apply_datetime(Utc.ymd(2019, 2, 1).and_hms(0, 0, 0)), + Some(Utc.ymd(2019, 1, 1).and_hms(0, 0, 0)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .months(-2) + .build() + .apply_datetime(Utc.ymd(2019, 2, 1).and_hms(0, 0, 0)), + Some(Utc.ymd(2018, 12, 1).and_hms(0, 0, 0)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .months(-1) + .build() + .apply_datetime(Utc.ymd(2019, 1, 31).and_hms(0, 0, 0)), + Some(Utc.ymd(2018, 12, 31).and_hms(0, 0, 0)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .months(-2) + .build() + .apply_datetime(Utc.ymd(2019, 1, 31).and_hms(0, 0, 0)), + None + ); + } + + #[test] + fn time_delta_apply_years() { + // plus + assert_eq!( + TimeDeltaBuilder::default() + .years(1) + .build() + .apply_datetime(Utc.ymd(2019, 1, 1).and_hms(0, 0, 0)), + Some(Utc.ymd(2020, 1, 1).and_hms(0, 0, 0)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .years(1) + .build() + .apply_datetime(Utc.ymd(2020, 2, 29).and_hms(0, 0, 0)), + None + ); + + // minus + assert_eq!( + TimeDeltaBuilder::default() + .years(-1) + .build() + .apply_datetime(Utc.ymd(2019, 1, 1).and_hms(0, 0, 0)), + Some(Utc.ymd(2018, 1, 1).and_hms(0, 0, 0)) + ); + + assert_eq!( + TimeDeltaBuilder::default() + .years(-1) + .build() + .apply_datetime(Utc.ymd(2020, 2, 29).and_hms(0, 0, 0)), + None + ); + } +} + +#[cfg(test)] +mod builder_tests { + use super::{TimeDelta, TimeDeltaBuilder}; + + #[test] + fn time_delta_builder() { + assert_eq!( + TimeDeltaBuilder::default() + .years(2019) + .months(6) + .days(10) + .hours(20) + .minutes(12) + .seconds(34) + .microseconds(56) + .build(), + TimeDelta::new(2019, 6, 10, 20, 12, 34, 56) + ); + } +} diff --git a/timedelta/src/lib.rs b/timedelta/src/lib.rs new file mode 100644 index 0000000..9211220 --- /dev/null +++ b/timedelta/src/lib.rs @@ -0,0 +1,5 @@ +mod delta; + +pub use delta::ApplyDateTime; +pub use delta::TimeDelta; +pub use delta::TimeDeltaBuilder;