├── .github
├── dependabot.yml
└── workflows
│ └── rust.yml
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── Makefile
├── README.md
├── image
└── txt_icon.jpg
└── src
├── command.rs
├── command
├── arguments.rs
├── arguments
│ ├── ambiguous_time_strategy.rs
│ ├── from.rs
│ ├── time.rs
│ └── to.rs
├── command_definition.rs
├── receiver.rs
├── validated_options.rs
└── validated_options
│ ├── ambiguous_time_strategy.rs
│ └── validated_user_inputs.rs
├── infrastructure.rs
├── infrastructure
├── current_local_timezone_provider.rs
└── current_local_timezone_provider
│ ├── get_system_timezone_from_env_var_tz.rs
│ ├── get_system_timezone_from_etc_localtime.rs
│ ├── get_system_timezone_from_etc_timezone.rs
│ └── local_timezone_string_provider.rs
├── main.rs
├── translator.rs
├── translator
└── translation_error.rs
├── validator.rs
└── validator
├── ambiguous_time_strategy_validator.rs
├── command_options_validator.rs
├── native_datetime_validator.rs
├── regex_matcher.rs
├── regex_matcher
├── ymd_hms_matcher.rs
├── ymd_matcher.rs
└── ymd_t_hms_matcher.rs
├── timezone_validator.rs
└── validation_error.rs
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "cargo"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 |
--------------------------------------------------------------------------------
/.github/workflows/rust.yml:
--------------------------------------------------------------------------------
1 | name: Build and Test
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | env:
10 | CARGO_TERM_COLOR: always
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ${{ matrix.os }}
16 |
17 | strategy:
18 | matrix:
19 | os: [ubuntu-latest, macos-latest]
20 |
21 | steps:
22 | - uses: actions/checkout@v4
23 | - name: Build
24 | run: cargo build --verbose
25 | - name: Check
26 | run: cargo check --verbose
27 | - name: Clippy
28 | run: cargo clippy --verbose
29 | - name: Run tests
30 | run: cargo test --verbose
31 | # TODO: Add show modules if GitHub Actions supports it
32 | # - name: Show Modules
33 | # run: cargo modules structure
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | .idea
3 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 4
4 |
5 | [[package]]
6 | name = "aho-corasick"
7 | version = "1.1.3"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
10 | dependencies = [
11 | "memchr",
12 | ]
13 |
14 | [[package]]
15 | name = "android-tzdata"
16 | version = "0.1.1"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
19 |
20 | [[package]]
21 | name = "android_system_properties"
22 | version = "0.1.5"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
25 | dependencies = [
26 | "libc",
27 | ]
28 |
29 | [[package]]
30 | name = "anstream"
31 | version = "0.6.14"
32 | source = "registry+https://github.com/rust-lang/crates.io-index"
33 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
34 | dependencies = [
35 | "anstyle",
36 | "anstyle-parse",
37 | "anstyle-query",
38 | "anstyle-wincon",
39 | "colorchoice",
40 | "is_terminal_polyfill",
41 | "utf8parse",
42 | ]
43 |
44 | [[package]]
45 | name = "anstyle"
46 | version = "1.0.8"
47 | source = "registry+https://github.com/rust-lang/crates.io-index"
48 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
49 |
50 | [[package]]
51 | name = "anstyle-parse"
52 | version = "0.2.4"
53 | source = "registry+https://github.com/rust-lang/crates.io-index"
54 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
55 | dependencies = [
56 | "utf8parse",
57 | ]
58 |
59 | [[package]]
60 | name = "anstyle-query"
61 | version = "1.1.0"
62 | source = "registry+https://github.com/rust-lang/crates.io-index"
63 | checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
64 | dependencies = [
65 | "windows-sys",
66 | ]
67 |
68 | [[package]]
69 | name = "anstyle-wincon"
70 | version = "3.0.3"
71 | source = "registry+https://github.com/rust-lang/crates.io-index"
72 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
73 | dependencies = [
74 | "anstyle",
75 | "windows-sys",
76 | ]
77 |
78 | [[package]]
79 | name = "assert_cmd"
80 | version = "2.0.17"
81 | source = "registry+https://github.com/rust-lang/crates.io-index"
82 | checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66"
83 | dependencies = [
84 | "anstyle",
85 | "bstr",
86 | "doc-comment",
87 | "libc",
88 | "predicates",
89 | "predicates-core",
90 | "predicates-tree",
91 | "wait-timeout",
92 | ]
93 |
94 | [[package]]
95 | name = "autocfg"
96 | version = "1.3.0"
97 | source = "registry+https://github.com/rust-lang/crates.io-index"
98 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
99 |
100 | [[package]]
101 | name = "bstr"
102 | version = "1.10.0"
103 | source = "registry+https://github.com/rust-lang/crates.io-index"
104 | checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c"
105 | dependencies = [
106 | "memchr",
107 | "regex-automata",
108 | "serde",
109 | ]
110 |
111 | [[package]]
112 | name = "bumpalo"
113 | version = "3.16.0"
114 | source = "registry+https://github.com/rust-lang/crates.io-index"
115 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
116 |
117 | [[package]]
118 | name = "cc"
119 | version = "1.0.99"
120 | source = "registry+https://github.com/rust-lang/crates.io-index"
121 | checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695"
122 |
123 | [[package]]
124 | name = "cfg-if"
125 | version = "1.0.0"
126 | source = "registry+https://github.com/rust-lang/crates.io-index"
127 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
128 |
129 | [[package]]
130 | name = "chrono"
131 | version = "0.4.41"
132 | source = "registry+https://github.com/rust-lang/crates.io-index"
133 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
134 | dependencies = [
135 | "android-tzdata",
136 | "iana-time-zone",
137 | "js-sys",
138 | "num-traits",
139 | "wasm-bindgen",
140 | "windows-link",
141 | ]
142 |
143 | [[package]]
144 | name = "chrono-tz"
145 | version = "0.10.3"
146 | source = "registry+https://github.com/rust-lang/crates.io-index"
147 | checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3"
148 | dependencies = [
149 | "chrono",
150 | "chrono-tz-build",
151 | "phf",
152 | ]
153 |
154 | [[package]]
155 | name = "chrono-tz-build"
156 | version = "0.4.0"
157 | source = "registry+https://github.com/rust-lang/crates.io-index"
158 | checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7"
159 | dependencies = [
160 | "parse-zoneinfo",
161 | "phf_codegen",
162 | ]
163 |
164 | [[package]]
165 | name = "clap"
166 | version = "4.5.37"
167 | source = "registry+https://github.com/rust-lang/crates.io-index"
168 | checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
169 | dependencies = [
170 | "clap_builder",
171 | ]
172 |
173 | [[package]]
174 | name = "clap_builder"
175 | version = "4.5.37"
176 | source = "registry+https://github.com/rust-lang/crates.io-index"
177 | checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
178 | dependencies = [
179 | "anstream",
180 | "anstyle",
181 | "clap_lex",
182 | "strsim",
183 | ]
184 |
185 | [[package]]
186 | name = "clap_lex"
187 | version = "0.7.4"
188 | source = "registry+https://github.com/rust-lang/crates.io-index"
189 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
190 |
191 | [[package]]
192 | name = "colorchoice"
193 | version = "1.0.1"
194 | source = "registry+https://github.com/rust-lang/crates.io-index"
195 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
196 |
197 | [[package]]
198 | name = "core-foundation-sys"
199 | version = "0.8.6"
200 | source = "registry+https://github.com/rust-lang/crates.io-index"
201 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
202 |
203 | [[package]]
204 | name = "difflib"
205 | version = "0.4.0"
206 | source = "registry+https://github.com/rust-lang/crates.io-index"
207 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
208 |
209 | [[package]]
210 | name = "doc-comment"
211 | version = "0.3.3"
212 | source = "registry+https://github.com/rust-lang/crates.io-index"
213 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
214 |
215 | [[package]]
216 | name = "float-cmp"
217 | version = "0.10.0"
218 | source = "registry+https://github.com/rust-lang/crates.io-index"
219 | checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
220 | dependencies = [
221 | "num-traits",
222 | ]
223 |
224 | [[package]]
225 | name = "iana-time-zone"
226 | version = "0.1.60"
227 | source = "registry+https://github.com/rust-lang/crates.io-index"
228 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
229 | dependencies = [
230 | "android_system_properties",
231 | "core-foundation-sys",
232 | "iana-time-zone-haiku",
233 | "js-sys",
234 | "wasm-bindgen",
235 | "windows-core",
236 | ]
237 |
238 | [[package]]
239 | name = "iana-time-zone-haiku"
240 | version = "0.1.2"
241 | source = "registry+https://github.com/rust-lang/crates.io-index"
242 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
243 | dependencies = [
244 | "cc",
245 | ]
246 |
247 | [[package]]
248 | name = "is_terminal_polyfill"
249 | version = "1.70.0"
250 | source = "registry+https://github.com/rust-lang/crates.io-index"
251 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
252 |
253 | [[package]]
254 | name = "js-sys"
255 | version = "0.3.69"
256 | source = "registry+https://github.com/rust-lang/crates.io-index"
257 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
258 | dependencies = [
259 | "wasm-bindgen",
260 | ]
261 |
262 | [[package]]
263 | name = "libc"
264 | version = "0.2.155"
265 | source = "registry+https://github.com/rust-lang/crates.io-index"
266 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
267 |
268 | [[package]]
269 | name = "log"
270 | version = "0.4.21"
271 | source = "registry+https://github.com/rust-lang/crates.io-index"
272 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
273 |
274 | [[package]]
275 | name = "memchr"
276 | version = "2.7.2"
277 | source = "registry+https://github.com/rust-lang/crates.io-index"
278 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
279 |
280 | [[package]]
281 | name = "normalize-line-endings"
282 | version = "0.3.0"
283 | source = "registry+https://github.com/rust-lang/crates.io-index"
284 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
285 |
286 | [[package]]
287 | name = "num-traits"
288 | version = "0.2.19"
289 | source = "registry+https://github.com/rust-lang/crates.io-index"
290 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
291 | dependencies = [
292 | "autocfg",
293 | ]
294 |
295 | [[package]]
296 | name = "once_cell"
297 | version = "1.19.0"
298 | source = "registry+https://github.com/rust-lang/crates.io-index"
299 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
300 |
301 | [[package]]
302 | name = "parse-zoneinfo"
303 | version = "0.3.1"
304 | source = "registry+https://github.com/rust-lang/crates.io-index"
305 | checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
306 | dependencies = [
307 | "regex",
308 | ]
309 |
310 | [[package]]
311 | name = "phf"
312 | version = "0.11.2"
313 | source = "registry+https://github.com/rust-lang/crates.io-index"
314 | checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
315 | dependencies = [
316 | "phf_shared",
317 | ]
318 |
319 | [[package]]
320 | name = "phf_codegen"
321 | version = "0.11.2"
322 | source = "registry+https://github.com/rust-lang/crates.io-index"
323 | checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
324 | dependencies = [
325 | "phf_generator",
326 | "phf_shared",
327 | ]
328 |
329 | [[package]]
330 | name = "phf_generator"
331 | version = "0.11.2"
332 | source = "registry+https://github.com/rust-lang/crates.io-index"
333 | checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
334 | dependencies = [
335 | "phf_shared",
336 | "rand",
337 | ]
338 |
339 | [[package]]
340 | name = "phf_shared"
341 | version = "0.11.2"
342 | source = "registry+https://github.com/rust-lang/crates.io-index"
343 | checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
344 | dependencies = [
345 | "siphasher",
346 | ]
347 |
348 | [[package]]
349 | name = "predicates"
350 | version = "3.1.3"
351 | source = "registry+https://github.com/rust-lang/crates.io-index"
352 | checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
353 | dependencies = [
354 | "anstyle",
355 | "difflib",
356 | "float-cmp",
357 | "normalize-line-endings",
358 | "predicates-core",
359 | "regex",
360 | ]
361 |
362 | [[package]]
363 | name = "predicates-core"
364 | version = "1.0.8"
365 | source = "registry+https://github.com/rust-lang/crates.io-index"
366 | checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931"
367 |
368 | [[package]]
369 | name = "predicates-tree"
370 | version = "1.0.11"
371 | source = "registry+https://github.com/rust-lang/crates.io-index"
372 | checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13"
373 | dependencies = [
374 | "predicates-core",
375 | "termtree",
376 | ]
377 |
378 | [[package]]
379 | name = "proc-macro2"
380 | version = "1.0.85"
381 | source = "registry+https://github.com/rust-lang/crates.io-index"
382 | checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
383 | dependencies = [
384 | "unicode-ident",
385 | ]
386 |
387 | [[package]]
388 | name = "quote"
389 | version = "1.0.36"
390 | source = "registry+https://github.com/rust-lang/crates.io-index"
391 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
392 | dependencies = [
393 | "proc-macro2",
394 | ]
395 |
396 | [[package]]
397 | name = "rand"
398 | version = "0.8.5"
399 | source = "registry+https://github.com/rust-lang/crates.io-index"
400 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
401 | dependencies = [
402 | "rand_core",
403 | ]
404 |
405 | [[package]]
406 | name = "rand_core"
407 | version = "0.6.4"
408 | source = "registry+https://github.com/rust-lang/crates.io-index"
409 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
410 |
411 | [[package]]
412 | name = "regex"
413 | version = "1.11.1"
414 | source = "registry+https://github.com/rust-lang/crates.io-index"
415 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
416 | dependencies = [
417 | "aho-corasick",
418 | "memchr",
419 | "regex-automata",
420 | "regex-syntax",
421 | ]
422 |
423 | [[package]]
424 | name = "regex-automata"
425 | version = "0.4.8"
426 | source = "registry+https://github.com/rust-lang/crates.io-index"
427 | checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
428 | dependencies = [
429 | "aho-corasick",
430 | "memchr",
431 | "regex-syntax",
432 | ]
433 |
434 | [[package]]
435 | name = "regex-syntax"
436 | version = "0.8.5"
437 | source = "registry+https://github.com/rust-lang/crates.io-index"
438 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
439 |
440 | [[package]]
441 | name = "serde"
442 | version = "1.0.204"
443 | source = "registry+https://github.com/rust-lang/crates.io-index"
444 | checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
445 | dependencies = [
446 | "serde_derive",
447 | ]
448 |
449 | [[package]]
450 | name = "serde_derive"
451 | version = "1.0.204"
452 | source = "registry+https://github.com/rust-lang/crates.io-index"
453 | checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
454 | dependencies = [
455 | "proc-macro2",
456 | "quote",
457 | "syn",
458 | ]
459 |
460 | [[package]]
461 | name = "siphasher"
462 | version = "0.3.11"
463 | source = "registry+https://github.com/rust-lang/crates.io-index"
464 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
465 |
466 | [[package]]
467 | name = "strsim"
468 | version = "0.11.1"
469 | source = "registry+https://github.com/rust-lang/crates.io-index"
470 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
471 |
472 | [[package]]
473 | name = "syn"
474 | version = "2.0.87"
475 | source = "registry+https://github.com/rust-lang/crates.io-index"
476 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
477 | dependencies = [
478 | "proc-macro2",
479 | "quote",
480 | "unicode-ident",
481 | ]
482 |
483 | [[package]]
484 | name = "termtree"
485 | version = "0.4.1"
486 | source = "registry+https://github.com/rust-lang/crates.io-index"
487 | checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
488 |
489 | [[package]]
490 | name = "thiserror"
491 | version = "2.0.12"
492 | source = "registry+https://github.com/rust-lang/crates.io-index"
493 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
494 | dependencies = [
495 | "thiserror-impl",
496 | ]
497 |
498 | [[package]]
499 | name = "thiserror-impl"
500 | version = "2.0.12"
501 | source = "registry+https://github.com/rust-lang/crates.io-index"
502 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
503 | dependencies = [
504 | "proc-macro2",
505 | "quote",
506 | "syn",
507 | ]
508 |
509 | [[package]]
510 | name = "tzt"
511 | version = "0.3.1"
512 | dependencies = [
513 | "assert_cmd",
514 | "chrono",
515 | "chrono-tz",
516 | "clap",
517 | "predicates",
518 | "regex",
519 | "thiserror",
520 | ]
521 |
522 | [[package]]
523 | name = "unicode-ident"
524 | version = "1.0.12"
525 | source = "registry+https://github.com/rust-lang/crates.io-index"
526 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
527 |
528 | [[package]]
529 | name = "utf8parse"
530 | version = "0.2.2"
531 | source = "registry+https://github.com/rust-lang/crates.io-index"
532 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
533 |
534 | [[package]]
535 | name = "wait-timeout"
536 | version = "0.2.0"
537 | source = "registry+https://github.com/rust-lang/crates.io-index"
538 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
539 | dependencies = [
540 | "libc",
541 | ]
542 |
543 | [[package]]
544 | name = "wasm-bindgen"
545 | version = "0.2.92"
546 | source = "registry+https://github.com/rust-lang/crates.io-index"
547 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
548 | dependencies = [
549 | "cfg-if",
550 | "wasm-bindgen-macro",
551 | ]
552 |
553 | [[package]]
554 | name = "wasm-bindgen-backend"
555 | version = "0.2.92"
556 | source = "registry+https://github.com/rust-lang/crates.io-index"
557 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
558 | dependencies = [
559 | "bumpalo",
560 | "log",
561 | "once_cell",
562 | "proc-macro2",
563 | "quote",
564 | "syn",
565 | "wasm-bindgen-shared",
566 | ]
567 |
568 | [[package]]
569 | name = "wasm-bindgen-macro"
570 | version = "0.2.92"
571 | source = "registry+https://github.com/rust-lang/crates.io-index"
572 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
573 | dependencies = [
574 | "quote",
575 | "wasm-bindgen-macro-support",
576 | ]
577 |
578 | [[package]]
579 | name = "wasm-bindgen-macro-support"
580 | version = "0.2.92"
581 | source = "registry+https://github.com/rust-lang/crates.io-index"
582 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
583 | dependencies = [
584 | "proc-macro2",
585 | "quote",
586 | "syn",
587 | "wasm-bindgen-backend",
588 | "wasm-bindgen-shared",
589 | ]
590 |
591 | [[package]]
592 | name = "wasm-bindgen-shared"
593 | version = "0.2.92"
594 | source = "registry+https://github.com/rust-lang/crates.io-index"
595 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
596 |
597 | [[package]]
598 | name = "windows-core"
599 | version = "0.52.0"
600 | source = "registry+https://github.com/rust-lang/crates.io-index"
601 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
602 | dependencies = [
603 | "windows-targets",
604 | ]
605 |
606 | [[package]]
607 | name = "windows-link"
608 | version = "0.1.0"
609 | source = "registry+https://github.com/rust-lang/crates.io-index"
610 | checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
611 |
612 | [[package]]
613 | name = "windows-sys"
614 | version = "0.52.0"
615 | source = "registry+https://github.com/rust-lang/crates.io-index"
616 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
617 | dependencies = [
618 | "windows-targets",
619 | ]
620 |
621 | [[package]]
622 | name = "windows-targets"
623 | version = "0.52.5"
624 | source = "registry+https://github.com/rust-lang/crates.io-index"
625 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
626 | dependencies = [
627 | "windows_aarch64_gnullvm",
628 | "windows_aarch64_msvc",
629 | "windows_i686_gnu",
630 | "windows_i686_gnullvm",
631 | "windows_i686_msvc",
632 | "windows_x86_64_gnu",
633 | "windows_x86_64_gnullvm",
634 | "windows_x86_64_msvc",
635 | ]
636 |
637 | [[package]]
638 | name = "windows_aarch64_gnullvm"
639 | version = "0.52.5"
640 | source = "registry+https://github.com/rust-lang/crates.io-index"
641 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
642 |
643 | [[package]]
644 | name = "windows_aarch64_msvc"
645 | version = "0.52.5"
646 | source = "registry+https://github.com/rust-lang/crates.io-index"
647 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
648 |
649 | [[package]]
650 | name = "windows_i686_gnu"
651 | version = "0.52.5"
652 | source = "registry+https://github.com/rust-lang/crates.io-index"
653 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
654 |
655 | [[package]]
656 | name = "windows_i686_gnullvm"
657 | version = "0.52.5"
658 | source = "registry+https://github.com/rust-lang/crates.io-index"
659 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
660 |
661 | [[package]]
662 | name = "windows_i686_msvc"
663 | version = "0.52.5"
664 | source = "registry+https://github.com/rust-lang/crates.io-index"
665 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
666 |
667 | [[package]]
668 | name = "windows_x86_64_gnu"
669 | version = "0.52.5"
670 | source = "registry+https://github.com/rust-lang/crates.io-index"
671 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
672 |
673 | [[package]]
674 | name = "windows_x86_64_gnullvm"
675 | version = "0.52.5"
676 | source = "registry+https://github.com/rust-lang/crates.io-index"
677 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
678 |
679 | [[package]]
680 | name = "windows_x86_64_msvc"
681 | version = "0.52.5"
682 | source = "registry+https://github.com/rust-lang/crates.io-index"
683 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
684 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "tzt"
3 | version = "0.3.1"
4 | edition = "2021"
5 | license = "MIT"
6 | description = "simple command-line utility that converts a given time from one timezone to another."
7 | homepage = "https://github.com/shunsock/timezone_translator"
8 | repository = "https://github.com/shunsock/timezone_translator"
9 | documentation = "https://github.com/shunsock/timezone_translator/blob/main/README.md"
10 | readme = "README.md"
11 | keywords = ["timezone", "time", "converter", "translator", "cli"]
12 | categories = ["command-line-utilities"]
13 |
14 | [dependencies]
15 | assert_cmd = "2.0"
16 | chrono = "0.4"
17 | chrono-tz = "0.10"
18 | clap = "4.5"
19 | predicates = "3.1"
20 | regex = "1.11.1"
21 | thiserror = "2.0.12"
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012-2024 Scott Chacon and others
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: install uninstall test
2 |
3 | install:
4 | cargo build --release
5 | sudo cp target/release/timezone_translator /usr/local/bin/tzt
6 |
7 | uninstall:
8 | sudo rm /usr/local/bin/tzt
9 |
10 | test:
11 | cargo build
12 | cargo fmt
13 | cargo clippy
14 | cargo test
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 | tzt - Timezone Translator
9 |
10 |
11 | simple command-line utility that converts a given time from one timezone to another.
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | ## Features
24 | - Convert a given time from one timezone to another.
25 | - Supports multiple timezones.
26 | - if you want to see the list of supported timezones, read following url.
27 | - https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html
28 |
29 | ## Usage
30 | You can use the following command to see the help message.
31 |
32 | ```bash
33 | $tzt --help
34 | translate time from one timezone to another
35 |
36 | Usage: tzt [OPTIONS] --time
37 |
38 | Options:
39 | -T, --time
40 | Time in the format YYYY-MM-DD HH:MM:SS (you can omit HH:MM:SS) or YYYY-MM-DDTHH:MM:SS
41 | -f, --from
42 | The original timezone (e.g. America/New_York) @see https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html [default: Your_Local_Timezone]
43 | -t, --to
44 | The target timezone (e.g. Asia/Tokyo) @see https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html# [default: Your_Local_Timezone]
45 | -a, --ambiguous-time-strategy
46 | Strategy to use for ambiguous times (earliest, latest) [default: earliest]
47 | -h, --help
48 | Print help
49 | -V, --version
50 | Print version
51 | ```
52 |
53 | ## Dependencies
54 | This project requires the following dependencies:
55 |
56 | - `Cargo`: The Rust package manager and build tool.
57 | - `Make`: A build automation tool that simplifies the build process.
58 |
59 | ## Getting Started
60 | ### Install (cargo)
61 | You can install the binary with cargo.
62 |
63 | ```bash
64 | cargo install tzt
65 | ```
66 |
67 | ### Install (binary)
68 | To install the binary, you can use the following command.
69 |
70 | ```bash
71 | sudo curl -L -o \
72 | /usr/local/bin/tzt \
73 | https://github.com/shunsock/timezone_translator/releases/download/v0.3.0/timezone_translator &&\
74 | sudo chmod +x /usr/local/bin/tzt
75 | ```
76 |
77 | ### Install (from source)
78 | You can also build and install the binary from source.
79 | To build and install the project, you can use the `install` target in the Makefile.
80 |
81 | ```bash
82 | git clone https://github.com/shunsock/timezone_translator.git
83 | cd timezone_translator
84 | make install
85 | ```
86 |
87 | After installing the binary, you can run it from the command line:
88 |
89 | ```bash
90 | $ tzt -T "2024-01-01 12:00:00" -f "America/New_York" -t "UTC"
91 | 2024-01-01 17:00:00 UTC
92 | ```
93 |
94 | ### Uninstalling
95 | You can uninstall the binary with cargo command.
96 |
97 | ```bash
98 | cargo uninstall tzt
99 | ```
100 |
101 | To remove the installed binary, use the `uninstall` command
102 | if you installed by curl or built from source.
103 |
104 | ```bash
105 | make uninstall
106 | ```
107 |
108 | ## Ambiguous Time Strategy
109 | There are two strategies for ambiguous times: `earliest` and `latest` to handle ambiguous times.
110 |
111 | Ambiguous times occur when the clocks are set back for daylight saving time (DST). When DST starts, the clock forwards by one hour, and when DST ends, the clock moves back by one hour. This means that there is one hour that occurs twice in the fall when the clock moves back. The `earliest` strategy uses the first occurrence of the time, and the `latest` strategy uses the second occurrence of the time.
112 |
113 | tzt use earliest strategy for ambiguous times by default.
114 | ```bash
115 | $ tzt --time '2024-11-03 01:30:00' --from 'America/New_York' --to 'UTC'
116 | 2024-11-03 05:30:00 UTC
117 | ```
118 |
119 | If you want to use latest strategy, you can use `ambiguous-time-strategy` (`-a`) option.
120 | ```bash
121 | $ tzt --time '2024-11-03 01:30:00' --from 'America/New_York' --to 'UTC' --ambiguous-time-strategy 'latest'
122 | 2024-11-03 06:30:00 UTC
123 | ```
124 |
125 | ## Error Handling
126 | tzt output Validation Error when `tzt validator` finds invalid value.
127 |
128 | this is an example of an invalid time format. you can see all valid time formats by using `tzt --help`.
129 | ```bash
130 | $ tzt --time '2024-01-' --from 'America/New_York' --to 'UTC'
131 | Invalid time format found: 2024-01- (expected: YYYY-MM-DD hh:mm:ss)
132 | ```
133 |
134 | this is an example of an invalid timezone. you can check all valid inputs by looking `https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html` because, tzt uses `chrono-tz` library internally.
135 | ```bash
136 | $ tzt --time '2024-03-10 02:30:00' --from 'America/New_York' --to 'NOT EXIST'
137 | Invalid timezone found: NOT EXIST. @see https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html
138 | ```
139 |
140 | `tzt translator` can handle the case where the output time and timezone do not exist.
141 | ```bash
142 | $ tzt --time '2024-03-10 02:30:00' --from 'America/New_York' --to 'America/Los_Angeles'
143 | Translation Error: Output time and timezone does not exist. Please check DST rules.
144 | ```
145 |
146 | ## LICENSE
147 | This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
148 |
--------------------------------------------------------------------------------
/image/txt_icon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shunsock/timezone_translator/79cdaa94f3e4a33b70681a9fa4ddb972f983cfb1/image/txt_icon.jpg
--------------------------------------------------------------------------------
/src/command.rs:
--------------------------------------------------------------------------------
1 | mod arguments;
2 | mod command_definition;
3 | pub(super) mod receiver;
4 | pub(crate) mod validated_options;
5 |
--------------------------------------------------------------------------------
/src/command/arguments.rs:
--------------------------------------------------------------------------------
1 | pub(crate) mod ambiguous_time_strategy;
2 | pub(crate) mod from;
3 | pub(crate) mod time;
4 | pub(crate) mod to;
5 |
--------------------------------------------------------------------------------
/src/command/arguments/ambiguous_time_strategy.rs:
--------------------------------------------------------------------------------
1 | use clap::Arg;
2 |
3 | pub(crate) fn ambiguous_time_strategy() -> Arg {
4 | Arg::new("ambiguous_time_strategy")
5 | .short('a')
6 | .long("ambiguous-time-strategy")
7 | .value_name("STRATEGY")
8 | .help("Strategy to use for ambiguous times (earliest, latest)")
9 | .default_value("earliest")
10 | .required(false)
11 | }
12 |
--------------------------------------------------------------------------------
/src/command/arguments/from.rs:
--------------------------------------------------------------------------------
1 | use clap::Arg;
2 |
3 | pub(crate) fn from(timezone: &'static str) -> Arg {
4 | Arg::new("from_timezone")
5 | .short('f')
6 | .long("from")
7 | .value_name("FROM_TIMEZONE")
8 | .help("The original timezone (e.g. America/New_York) @see https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html")
9 | .required(false)
10 | .default_value(timezone)
11 | }
12 |
--------------------------------------------------------------------------------
/src/command/arguments/time.rs:
--------------------------------------------------------------------------------
1 | use clap::Arg;
2 |
3 | pub(crate) fn time() -> Arg {
4 | Arg::new("time")
5 | .short('T')
6 | .long("time")
7 | .value_name("TIME")
8 | .help(
9 | "Time in the format YYYY-MM-DD HH:MM:SS (you can omit HH:MM:SS) or YYYY-MM-DDTHH:MM:SS",
10 | )
11 | .required(true)
12 | }
13 |
--------------------------------------------------------------------------------
/src/command/arguments/to.rs:
--------------------------------------------------------------------------------
1 | use clap::Arg;
2 |
3 | pub(crate) fn to(timezone: &'static str) -> Arg {
4 | Arg::new("to_timezone")
5 | .short('t')
6 | .long("to")
7 | .value_name("TO_TIMEZONE")
8 | .help("The target timezone (e.g. Asia/Tokyo) @see https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html#")
9 | .required(false)
10 | .default_value(timezone)
11 | }
12 |
--------------------------------------------------------------------------------
/src/command/command_definition.rs:
--------------------------------------------------------------------------------
1 | use super::arguments::{
2 | ambiguous_time_strategy::ambiguous_time_strategy, from::from, time::time, to::to,
3 | };
4 | use crate::infrastructure::current_local_timezone_provider::local_timezone_string_provider::provide_local_timezone_string;
5 | use clap::Command;
6 |
7 | /// # About:
8 | /// Provides the command definition for the `tzt` command.
9 | ///
10 | /// # Returns:
11 | /// `Command` struct containing the command definition.
12 | ///
13 | /// # Example:
14 | /// ```
15 | /// use clap::ArgMatches;
16 | /// use command::command_definition::command_provider;
17 | /// let user_input: ArgMatches = command_provider().get_matches();
18 | /// ```
19 | pub(crate) fn command_provider() -> Command {
20 | let now: String = provide_local_timezone_string();
21 | let now_str: &'static str = Box::leak(now.into_boxed_str());
22 |
23 | Command::new("tzt - Timezone Translator")
24 | .version("0.3.1")
25 | .author("shunsock")
26 | .about("translate time from one timezone to another")
27 | .arg(time())
28 | .arg(from(now_str))
29 | .arg(to(now_str))
30 | .arg(ambiguous_time_strategy())
31 | }
32 |
--------------------------------------------------------------------------------
/src/command/receiver.rs:
--------------------------------------------------------------------------------
1 | use super::command_definition::command_provider;
2 | use clap::ArgMatches;
3 |
4 | /// # About:
5 | /// Receives user input from the command line and returns it as a `ArgMatches` struct.
6 | /// This function is used to receive user input from the command line.
7 | ///
8 | /// # Example:
9 | /// ```
10 | /// use clap::ArgMatches;
11 | /// use command::receiver::receive_user_input;
12 | /// let user_input: ArgMatches = receive_user_input();
13 | /// ```
14 | ///
15 | /// # Returns:
16 | /// `ArgMatches` struct containing the user input.
17 | ///
18 | /// # Note:
19 | /// if you want to change command settings, see `command_definition` module.
20 | pub(crate) fn receive_user_input() -> ArgMatches {
21 | command_provider().get_matches()
22 | }
23 |
--------------------------------------------------------------------------------
/src/command/validated_options.rs:
--------------------------------------------------------------------------------
1 | pub(crate) mod ambiguous_time_strategy;
2 | pub(crate) mod validated_user_inputs;
3 |
--------------------------------------------------------------------------------
/src/command/validated_options/ambiguous_time_strategy.rs:
--------------------------------------------------------------------------------
1 | #[derive(Debug, Clone, Copy, PartialEq)]
2 | pub(crate) enum AmbiguousTimeStrategy {
3 | Earliest,
4 | Latest,
5 | }
6 |
--------------------------------------------------------------------------------
/src/command/validated_options/validated_user_inputs.rs:
--------------------------------------------------------------------------------
1 | use crate::command::validated_options::ambiguous_time_strategy::AmbiguousTimeStrategy;
2 | use chrono::NaiveDateTime;
3 | use chrono_tz::Tz;
4 |
5 | pub(crate) struct ValidatedCommandOptions {
6 | time: NaiveDateTime,
7 | from_tz: Tz,
8 | to_tz: Tz,
9 | ambiguous_time_strategy: AmbiguousTimeStrategy,
10 | }
11 |
12 | impl ValidatedCommandOptions {
13 | pub(crate) fn new(
14 | time: NaiveDateTime,
15 | from_tz: Tz,
16 | to_tz: Tz,
17 | ambiguous_time_strategy: AmbiguousTimeStrategy,
18 | ) -> Self {
19 | Self {
20 | time,
21 | from_tz,
22 | to_tz,
23 | ambiguous_time_strategy,
24 | }
25 | }
26 |
27 | pub(crate) fn get_param_time(&self) -> NaiveDateTime {
28 | self.time
29 | }
30 |
31 | pub(crate) fn get_param_from_tz(&self) -> Tz {
32 | self.from_tz
33 | }
34 |
35 | pub(crate) fn get_param_to_tz(&self) -> Tz {
36 | self.to_tz
37 | }
38 |
39 | pub(crate) fn ambiguous_time_strategy(&self) -> AmbiguousTimeStrategy {
40 | self.ambiguous_time_strategy
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/infrastructure.rs:
--------------------------------------------------------------------------------
1 | pub(crate) mod current_local_timezone_provider;
2 |
--------------------------------------------------------------------------------
/src/infrastructure/current_local_timezone_provider.rs:
--------------------------------------------------------------------------------
1 | mod get_system_timezone_from_env_var_tz;
2 | mod get_system_timezone_from_etc_localtime;
3 | mod get_system_timezone_from_etc_timezone;
4 | pub(crate) mod local_timezone_string_provider;
5 |
--------------------------------------------------------------------------------
/src/infrastructure/current_local_timezone_provider/get_system_timezone_from_env_var_tz.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 | use std::env::VarError;
3 |
4 | pub(crate) struct EnvironmentVariableTzProvider {
5 | env_name: String,
6 | }
7 |
8 | impl EnvironmentVariableTzProvider {
9 | pub(crate) fn new(env_var_name: Option) -> Self {
10 | match env_var_name {
11 | Some(env_name) => EnvironmentVariableTzProvider { env_name },
12 | None => EnvironmentVariableTzProvider {
13 | env_name: "TZ".to_string(),
14 | },
15 | }
16 | }
17 | }
18 |
19 | impl EnvironmentVariableTzProvider {
20 | pub(crate) fn get_env_var_tz(&self) -> Option {
21 | let timezone: Result = env::var(&self.env_name);
22 |
23 | match timezone {
24 | Ok(tz) => Some(tz),
25 | Err(_) => None,
26 | }
27 | }
28 | }
29 |
30 | #[cfg(test)]
31 | mod tests {
32 | use super::*;
33 | fn set_tmp_env_var() {
34 | env::set_var("TEST_TZ", "America/New_York");
35 | }
36 |
37 | fn remove_tmp_env_var() {
38 | env::remove_var("TEST_TZ");
39 | }
40 |
41 | #[test]
42 | fn test_get_env_var_tz() {
43 | set_tmp_env_var();
44 | let tz_provider = EnvironmentVariableTzProvider::new(Some("TEST_TZ".to_string()));
45 | assert_eq!(
46 | tz_provider.get_env_var_tz(),
47 | Some("America/New_York".to_string())
48 | );
49 | remove_tmp_env_var();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/infrastructure/current_local_timezone_provider/get_system_timezone_from_etc_localtime.rs:
--------------------------------------------------------------------------------
1 | use std::fs;
2 |
3 | pub(crate) fn get_system_timezone_from_etc_localtime() -> Option {
4 | return match fs::read_link("/etc/localtime") {
5 | Ok(path) => {
6 | let path_str = path.to_string_lossy();
7 | path_str
8 | .find("/zoneinfo/")
9 | .map(|pos| path_str[pos + "/zoneinfo/".len()..].to_string())
10 | }
11 | Err(_) => None,
12 | };
13 | }
14 |
--------------------------------------------------------------------------------
/src/infrastructure/current_local_timezone_provider/get_system_timezone_from_etc_timezone.rs:
--------------------------------------------------------------------------------
1 | use std::fs::File;
2 | use std::io::Read;
3 |
4 | /// Returns the system timezone from the `/etc/timezone` file contents as a `String`.
5 | ///
6 | pub(crate) fn get_system_timezone_from_etc_timezone() -> Option {
7 | let mut file: File = match File::open("/etc/timezone") {
8 | Ok(f) => f,
9 | Err(_) => return None,
10 | };
11 |
12 | let mut contents = String::new();
13 | if file.read_to_string(&mut contents).is_ok() {
14 | Some(contents.trim().to_string())
15 | } else {
16 | None
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/infrastructure/current_local_timezone_provider/local_timezone_string_provider.rs:
--------------------------------------------------------------------------------
1 | use super::get_system_timezone_from_env_var_tz::EnvironmentVariableTzProvider;
2 | use super::get_system_timezone_from_etc_localtime::get_system_timezone_from_etc_localtime;
3 | use super::get_system_timezone_from_etc_timezone::get_system_timezone_from_etc_timezone;
4 |
5 | /// Returns the name of the local timezone as a `String`.
6 | ///
7 | /// # Examples
8 | ///
9 | /// ```
10 | /// let timezone = local_timezone_string();
11 | /// println!("Local timezone: {}", timezone);
12 | /// ```
13 | ///
14 | /// # Panics
15 | ///
16 | /// if environment variable TZ and following file links are not found,
17 | /// this function return panic
18 | pub(crate) fn provide_local_timezone_string() -> String {
19 | // read environment variable TZ
20 | let env_var_tz: Option = EnvironmentVariableTzProvider::new(None).get_env_var_tz();
21 | if let Some(env_var_tz) = env_var_tz {
22 | return env_var_tz;
23 | }
24 |
25 | // read /etc/localtime
26 | let tz_from_etc_localtime: Option = get_system_timezone_from_etc_localtime();
27 | if let Some(tz_from_etc_localtime) = tz_from_etc_localtime {
28 | return tz_from_etc_localtime;
29 | }
30 |
31 | // read /etc/timezone
32 | let tz_from_etc_timezone: Option = get_system_timezone_from_etc_timezone();
33 | if let Some(tz_from_etc_timezone) = tz_from_etc_timezone {
34 | return tz_from_etc_timezone;
35 | }
36 |
37 | let error_message = "System Timezone Not Found:
38 | Could not find local timezone. Please set TZ environment variable.
39 | ";
40 | panic!("{}", error_message);
41 | }
42 |
43 | #[cfg(test)]
44 | mod tests {
45 | use super::*;
46 | use regex::Regex;
47 |
48 | #[test]
49 | fn test_check_output_match_timezone() {
50 | let local_timezone_str = provide_local_timezone_string();
51 | let re: Regex = Regex::new(r"^[a-zA-Z_/]+$").unwrap();
52 | assert!(re.is_match(&local_timezone_str));
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | mod command;
2 | mod infrastructure;
3 | mod translator;
4 | mod validator;
5 |
6 | use chrono::prelude::*;
7 | use chrono_tz::Tz;
8 | use clap::ArgMatches;
9 | use command::receiver::receive_user_input;
10 | use command::validated_options::validated_user_inputs::ValidatedCommandOptions;
11 | use std::process::exit;
12 | use translator::translation_error::TranslationError;
13 | use translator::TimezoneTranslator;
14 | use validator::command_options_validator::validate_command_options;
15 |
16 | fn main() {
17 | let user_input_options: ArgMatches = receive_user_input();
18 |
19 | let validated_options: ValidatedCommandOptions =
20 | match validate_command_options(&user_input_options) {
21 | Ok(v) => v,
22 | Err(e) => {
23 | eprintln!("{}", e);
24 | exit(1);
25 | }
26 | };
27 |
28 | let date_time_mapped: Result, TranslationError> = TimezoneTranslator::new(
29 | validated_options.get_param_time(),
30 | validated_options.get_param_from_tz(),
31 | validated_options.get_param_to_tz(),
32 | validated_options.ambiguous_time_strategy(),
33 | )
34 | .convert();
35 |
36 | match date_time_mapped {
37 | Ok(mapped) => {
38 | println!("{}", mapped);
39 | exit(0);
40 | }
41 | Err(e) => {
42 | eprintln!("{}", e);
43 | exit(1);
44 | }
45 | }
46 | }
47 |
48 | #[cfg(test)]
49 | mod tests {
50 | use assert_cmd::Command;
51 | use predicates::prelude::*;
52 |
53 | /// This test verifies that the program can correctly convert a given time
54 | /// from one timezone (America/New_York) to another (UTC).
55 | #[test]
56 | fn converts_time_from_new_york_to_utc() {
57 | let mut cmd = Command::cargo_bin("tzt").unwrap();
58 | cmd.args(&[
59 | "-T",
60 | "2024-01-01 12:00:00",
61 | "-f",
62 | "America/New_York",
63 | "-t",
64 | "UTC",
65 | ]);
66 | cmd.assert()
67 | .success()
68 | .stdout(predicate::str::contains("2024-01-01 17:00:00 UTC"));
69 | }
70 |
71 | #[test]
72 | fn converts_time_with_no_timezone() {
73 | let mut cmd = Command::cargo_bin("tzt").unwrap();
74 | cmd.args(&["-T", "2024-01-01 12:00:00"]);
75 | cmd.assert()
76 | .success()
77 | .stdout(predicate::str::contains("2024-01-01 12:00:00"));
78 | }
79 |
80 | /// This test verifies that the program correctly converts the given time
81 | /// from UTC to the specified timezone (Asia/Tokyo) and returns the
82 | /// corresponding standard time.
83 | ///
84 | /// Specifically, it checks that the output is not just the input timezone information,
85 | /// but properly represents the standard time in the target timezone (JST in this case).
86 | #[test]
87 | fn converts_time_from_utc_to_tokyo() {
88 | let mut cmd = Command::cargo_bin("tzt").unwrap();
89 | cmd.args(&["-T", "2024-01-01 12:00:00", "-f", "UTC", "-t", "Asia/Tokyo"]);
90 | cmd.assert()
91 | .success()
92 | .stdout(predicate::str::contains("2024-01-01 21:00:00 JST"));
93 | }
94 |
95 | /// This test verifies that the program can correctly handle input provided
96 | /// in the ISO 8601 format. ISO 8601 is an international standard for
97 | /// date and time representation, and it typically takes the form
98 | /// "YYYY-MM-DDTHH:MM:SS" (e.g., "2024-01-01T12:00:00").
99 | ///
100 | /// The test ensures that the program accurately parses the date and time
101 | /// from this format and converts it to the specified target timezone (UTC).
102 | ///
103 | /// For more details about ISO 8601 format, refer to the
104 | /// [Wikipedia article](https://en.wikipedia.org/wiki/ISO_8601).
105 | #[test]
106 | fn handles_iso_8601_format() {
107 | let mut cmd = Command::cargo_bin("tzt").unwrap();
108 | cmd.args(&[
109 | "-T",
110 | "2024-01-01T12:00:00",
111 | "-f",
112 | "America/New_York",
113 | "-t",
114 | "UTC",
115 | ]);
116 | cmd.assert()
117 | .success()
118 | .stdout(predicate::str::contains("2024-01-01 17:00:00 UTC"));
119 | }
120 |
121 | /// This test checks the program's handling of ambiguous times caused by
122 | /// daylight saving time (DST) changes. When DST ends, clocks are set back
123 | /// one hour, resulting in a repeated hour that can be ambiguous.
124 | /// This test uses the "earliest" strategy, which means that the program
125 | /// should select the first occurrence of the ambiguous time.
126 | ///
127 | /// For example, on November 3, 2024, in the "America/New_York" timezone,
128 | /// the time "01:30:00" can occur twice—once before the DST ends and once after.
129 | /// This test ensures that the program correctly handles this situation
130 | /// by selecting the earlier occurrence of "01:30:00".
131 | #[test]
132 | fn handles_ambiguous_time_with_earliest_strategy() {
133 | let mut cmd = Command::cargo_bin("tzt").unwrap();
134 | cmd.args(&[
135 | "--time",
136 | "2024-11-03 01:30:00",
137 | "--from",
138 | "America/New_York",
139 | "--to",
140 | "UTC",
141 | ]);
142 | cmd.assert()
143 | .success()
144 | .stdout(predicate::str::contains("2024-11-03 05:30:00 UTC"));
145 | }
146 |
147 | /// This test verifies the program's handling of ambiguous times due to
148 | /// daylight saving time (DST) changes, specifically using the "latest" strategy.
149 | /// When DST ends and clocks are set back one hour, an ambiguous time can occur
150 | /// twice. This strategy ensures that the program selects the second occurrence
151 | /// of the ambiguous time.
152 | ///
153 | /// For example, on November 3, 2024, in the "America/New_York" timezone,
154 | /// the time "01:30:00" occurs twice. The first occurrence happens during
155 | /// DST, and the second occurrence happens after DST ends. This test confirms
156 | /// that the program correctly handles this situation by selecting the later
157 | /// occurrence of "01:30:00".
158 | #[test]
159 | fn handles_ambiguous_time_with_latest_strategy() {
160 | let mut cmd = Command::cargo_bin("tzt").unwrap();
161 | cmd.args(&[
162 | "--time",
163 | "2024-11-03 01:30:00",
164 | "--from",
165 | "America/New_York",
166 | "--to",
167 | "UTC",
168 | "--ambiguous-time-strategy",
169 | "latest",
170 | ]);
171 | cmd.assert()
172 | .success()
173 | .stdout(predicate::str::contains("2024-11-03 06:30:00 UTC"));
174 | }
175 |
176 | /// This test validates that the program correctly identifies
177 | /// and reports an error when an invalid "from" timezone is provided.
178 | #[test]
179 | fn fails_with_invalid_from_timezone() {
180 | let mut cmd = Command::cargo_bin("tzt").unwrap();
181 | cmd.args(&["-T", "2024-01-01 12:00:00", "-f", "NOT_EXIST", "-t", "UTC"]);
182 | cmd.assert()
183 | .failure()
184 | .stderr(predicate::str::contains("Validation Error"));
185 | }
186 |
187 | /// This test checks that the program correctly identifies
188 | /// and reports an error when an invalid "to" timezone is provided.
189 | #[test]
190 | fn fails_with_invalid_to_timezone() {
191 | let mut cmd = Command::cargo_bin("tzt").unwrap();
192 | cmd.args(&["-T", "2024-01-01 12:00:00", "-f", "UTC", "-t", "NOT_EXIST"]);
193 | cmd.assert()
194 | .failure()
195 | .stderr(predicate::str::contains("Validation Error"));
196 | }
197 |
198 | /// This test verifies that the program correctly handles cases where the resulting
199 | /// time does not exist due to daylight saving time (DST) transitions. Specifically,
200 | /// during the spring transition, clocks are set forward one hour, causing a gap in time
201 | /// where certain times do not exist.
202 | ///
203 | /// For example, on March 10, 2024, in the "America/New_York" timezone, the local time
204 | /// skips from 02:00:00 to 03:00:00, meaning that any time between 02:00:00 and 02:59:59
205 | /// does not exist on that day. This test checks that the program correctly identifies
206 | /// this scenario and returns an appropriate error, ensuring robust handling of such
207 | /// edge cases.
208 | ///
209 | /// The expected behavior is for the program to detect the non-existent time and
210 | /// provide a clear error message indicating the issue.
211 | #[test]
212 | fn fails_nonexistent_time_due_to_dst() {
213 | let mut cmd = Command::cargo_bin("tzt").unwrap();
214 | cmd.args(&[
215 | "--time",
216 | "2024-03-10 02:30:00",
217 | "--from",
218 | "America/New_York",
219 | "--to",
220 | "America/Los_Angeles",
221 | ]);
222 | cmd.assert()
223 | .failure()
224 | .stderr(predicate::str::contains("Translation Error"));
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/src/translator.rs:
--------------------------------------------------------------------------------
1 | pub(super) mod translation_error;
2 |
3 | use crate::command::validated_options::ambiguous_time_strategy::AmbiguousTimeStrategy;
4 | use chrono::{DateTime, LocalResult, MappedLocalTime, NaiveDateTime, TimeZone};
5 | use chrono_tz::Tz;
6 | use translation_error::TranslationError;
7 |
8 | pub(crate) struct TimezoneTranslator {
9 | time: NaiveDateTime,
10 | from_tz: Tz,
11 | to_tz: Tz,
12 | ambiguous_time_strategy: AmbiguousTimeStrategy,
13 | }
14 |
15 | impl TimezoneTranslator {
16 | pub(crate) fn new(
17 | time: NaiveDateTime,
18 | from_tz: Tz,
19 | to_tz: Tz,
20 | ambiguous_time_strategy: AmbiguousTimeStrategy,
21 | ) -> Self {
22 | Self {
23 | time,
24 | from_tz,
25 | to_tz,
26 | ambiguous_time_strategy,
27 | }
28 | }
29 |
30 | pub(crate) fn convert(&self) -> Result, TranslationError> {
31 | // Extract the time from the `time` field with `from_tz` field
32 | let mapped: MappedLocalTime> = self.from_tz.from_local_datetime(&self.time);
33 |
34 | match mapped {
35 | LocalResult::Single(time) => Ok(time.with_timezone(&self.to_tz)),
36 | LocalResult::Ambiguous(time_earliest, time_latest) => {
37 | Ok(select_time_with_ambiguous_time_strategy(
38 | self.ambiguous_time_strategy,
39 | self.to_tz,
40 | time_earliest,
41 | time_latest,
42 | ))
43 | }
44 | LocalResult::None => {
45 | let error = TranslationError::TranslationError {
46 | time: self.time,
47 | from_tz: self.from_tz,
48 | to_tz: self.to_tz,
49 | };
50 | Err(error)
51 | }
52 | }
53 | }
54 | }
55 |
56 | fn select_time_with_ambiguous_time_strategy(
57 | strategy: AmbiguousTimeStrategy,
58 | timezone: Tz,
59 | time_earliest: DateTime,
60 | time_latest: DateTime,
61 | ) -> DateTime {
62 | match strategy {
63 | AmbiguousTimeStrategy::Earliest => time_earliest.with_timezone(&timezone),
64 | AmbiguousTimeStrategy::Latest => time_latest.with_timezone(&timezone),
65 | }
66 | }
67 |
68 | #[cfg(test)]
69 | mod tests {
70 | use super::*;
71 | use chrono::{NaiveDate, Utc};
72 | use chrono_tz::Tz;
73 |
74 | /// This test checks if the `TimezoneTranslator` struct is created correctly.
75 | /// It checks if the `time`, `from_tz`, and `to_tz` fields are set correctly.
76 | /// expected: `TimezoneTranslator`
77 | #[test]
78 | fn test_new() {
79 | let date: NaiveDate = NaiveDate::from_ymd_opt(2024, 6, 27).unwrap();
80 | let time: NaiveDateTime = date.and_hms_opt(12, 0, 0).unwrap();
81 | let from_tz: Tz = "America/New_York".parse().unwrap();
82 | let to_tz: Tz = "Europe/London".parse().unwrap();
83 | let ambiguous_time_strategy: AmbiguousTimeStrategy = AmbiguousTimeStrategy::Earliest;
84 |
85 | let translator: TimezoneTranslator =
86 | TimezoneTranslator::new(time, from_tz, to_tz, ambiguous_time_strategy);
87 |
88 | assert_eq!(translator.time, time);
89 | assert_eq!(translator.from_tz, from_tz);
90 | assert_eq!(translator.to_tz, to_tz);
91 | assert_eq!(translator.ambiguous_time_strategy, ambiguous_time_strategy);
92 | }
93 |
94 | /// This test checks if the `convert` method works correctly.
95 | /// It checks if the method returns a `DateTime` object.
96 | /// expected: `DateTime`
97 | #[test]
98 | fn test_convert() {
99 | // input for the test
100 | let date: NaiveDate = NaiveDate::from_ymd_opt(2024, 6, 27).unwrap();
101 | let time: NaiveDateTime = date.and_hms_opt(12, 0, 0).unwrap();
102 | let from_tz: Tz = "America/New_York".parse().unwrap();
103 | let to_tz: Tz = "UTC".parse().unwrap();
104 | let ambiguous_time_strategy: AmbiguousTimeStrategy = AmbiguousTimeStrategy::Earliest;
105 |
106 | // expected result
107 | // +4 hours from America/New_York to UTC
108 | let expected_time = Utc
109 | .with_ymd_and_hms(2024, 6, 27, 16, 0, 0)
110 | .unwrap()
111 | .with_timezone(&to_tz);
112 |
113 | // calculate the actual result
114 | let translator: TimezoneTranslator =
115 | TimezoneTranslator::new(time, from_tz, to_tz, ambiguous_time_strategy);
116 | let actual_converted_time: Result, TranslationError> = translator.convert();
117 |
118 | assert!(actual_converted_time.is_ok());
119 |
120 | // confirm if the actual result is the same as the expected result
121 | assert_eq!(actual_converted_time.unwrap(), expected_time);
122 | }
123 |
124 | /// This test check option `AmbiguousTimeStrategy::Earliest` works correctly.
125 | /// American/New_York is UTC-4 but `2024-11-03 01:30:00` is ambiguous. (after DST ends)
126 | /// `2024-11-03 05:30:00` is the earliest time.
127 | /// expected: `DateTime`
128 | #[test]
129 | fn test_earliest_ambiguous_time_strategy() {
130 | // input for the test
131 | let date: NaiveDate = NaiveDate::from_ymd_opt(2024, 11, 03).unwrap();
132 | let time: NaiveDateTime = date.and_hms_opt(01, 30, 0).unwrap();
133 | let from_tz: Tz = "America/New_York".parse().unwrap();
134 | let to_tz: Tz = "UTC".parse().unwrap();
135 | let ambiguous_time_strategy: AmbiguousTimeStrategy = AmbiguousTimeStrategy::Earliest;
136 |
137 | // expected result
138 | // +4, +5 hours from America/New_York to UTC (DST ends)
139 | // in this case, the earliest time is 5:30
140 | let expected_time = Utc
141 | .with_ymd_and_hms(2024, 11, 03, 5, 30, 0)
142 | .unwrap()
143 | .with_timezone(&to_tz);
144 |
145 | // calculate the actual result
146 | let translator: TimezoneTranslator =
147 | TimezoneTranslator::new(time, from_tz, to_tz, ambiguous_time_strategy);
148 | let actual_converted_time: Result, TranslationError> = translator.convert();
149 |
150 | assert!(actual_converted_time.is_ok());
151 |
152 | // confirm if the actual result is the same as the expected result
153 | assert_eq!(actual_converted_time.unwrap(), expected_time);
154 | }
155 |
156 | /// This test check option `AmbiguousTimeStrategy::Latest` works correctly.
157 | /// American/New_York is UTC-4 but `2024-11-03 01:30:00` is ambiguous. (after DST ends)
158 | /// `2024-11-03 06:30:00` is the latest time.
159 | /// expected: `DateTime`
160 | #[test]
161 | fn test_latest_ambiguous_time_strategy() {
162 | // input for the test
163 | let date: NaiveDate = NaiveDate::from_ymd_opt(2024, 11, 03).unwrap();
164 | let time: NaiveDateTime = date.and_hms_opt(01, 30, 0).unwrap();
165 | let from_tz: Tz = "America/New_York".parse().unwrap();
166 | let to_tz: Tz = "UTC".parse().unwrap();
167 | let ambiguous_time_strategy: AmbiguousTimeStrategy = AmbiguousTimeStrategy::Latest;
168 |
169 | // expected result
170 | // +4, +5 hours from America/New_York to UTC (DST ends)
171 | // in this case, the latest time is 6:30
172 | let expected_time = Utc
173 | .with_ymd_and_hms(2024, 11, 03, 6, 30, 0)
174 | .unwrap()
175 | .with_timezone(&to_tz);
176 |
177 | // calculate the actual result
178 | let translator: TimezoneTranslator =
179 | TimezoneTranslator::new(time, from_tz, to_tz, ambiguous_time_strategy);
180 | let actual_converted_time: Result, TranslationError> = translator.convert();
181 |
182 | assert!(actual_converted_time.is_ok());
183 |
184 | // confirm if the actual result is the same as the expected result
185 | assert_eq!(actual_converted_time.unwrap(), expected_time);
186 | }
187 |
188 | /// This test checks if the `convert` method returns an error when the output time does not exist.
189 | /// expected: `TranslationError`
190 | #[test]
191 | fn test_output_timestamp_does_not_exist() {
192 | // input for the test
193 | let date: NaiveDate = NaiveDate::from_ymd_opt(2024, 03, 10).unwrap();
194 | let time: NaiveDateTime = date.and_hms_opt(02, 30, 0).unwrap();
195 | let from_tz: Tz = "America/New_York".parse().unwrap();
196 | let to_tz: Tz = "America/Los_Angeles".parse().unwrap();
197 | let ambiguous_time_strategy: AmbiguousTimeStrategy = AmbiguousTimeStrategy::Latest;
198 |
199 | // calculate the actual result
200 | let translator: TimezoneTranslator =
201 | TimezoneTranslator::new(time, from_tz, to_tz, ambiguous_time_strategy);
202 | let actual_converted_time: Result, TranslationError> = translator.convert();
203 |
204 | // check result is error
205 | assert!(actual_converted_time.is_err());
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/src/translator/translation_error.rs:
--------------------------------------------------------------------------------
1 | use chrono::NaiveDateTime;
2 | use chrono_tz::Tz;
3 |
4 | #[derive(thiserror::Error, Debug)]
5 | pub(crate) enum TranslationError {
6 | #[error("Translation Error: Output time and timezone does not exist. Please check DST rules.")]
7 | TranslationError {
8 | time: NaiveDateTime,
9 | from_tz: Tz,
10 | to_tz: Tz,
11 | },
12 | }
13 |
--------------------------------------------------------------------------------
/src/validator.rs:
--------------------------------------------------------------------------------
1 | pub(crate) mod ambiguous_time_strategy_validator;
2 | pub(super) mod command_options_validator;
3 | mod native_datetime_validator;
4 | mod regex_matcher;
5 | mod timezone_validator;
6 | mod validation_error;
7 |
--------------------------------------------------------------------------------
/src/validator/ambiguous_time_strategy_validator.rs:
--------------------------------------------------------------------------------
1 | use crate::command::validated_options::ambiguous_time_strategy::AmbiguousTimeStrategy;
2 | use crate::validator::validation_error::ValidationError;
3 |
4 | pub(super) fn validate_string_for_ambiguous_time_strategy(
5 | ambiguous_time_strategy_str: &str,
6 | ) -> Result {
7 | match ambiguous_time_strategy_str {
8 | "earliest" => Ok(AmbiguousTimeStrategy::Earliest),
9 | "latest" => Ok(AmbiguousTimeStrategy::Latest),
10 | _ => Err(ValidationError::AmbiguousTimeStrategy {
11 | ambiguous_time_strategy: ambiguous_time_strategy_str.to_string(),
12 | }),
13 | }
14 | }
15 |
16 | #[cfg(test)]
17 | mod tests {
18 | use super::*;
19 |
20 | /// Test that the function `validate_string_for_ambiguous_time_strategy` returns the correct
21 | /// expect: `AmbiguousTimeStrategy`
22 | #[test]
23 | fn test_validate_string_for_ambiguous_time_strategy() {
24 | assert_eq!(
25 | validate_string_for_ambiguous_time_strategy("earliest").unwrap(),
26 | AmbiguousTimeStrategy::Earliest
27 | );
28 | assert_eq!(
29 | validate_string_for_ambiguous_time_strategy("latest").unwrap(),
30 | AmbiguousTimeStrategy::Latest
31 | );
32 | }
33 |
34 | /// Test that the function `validate_string_for_ambiguous_time_strategy` returns an error
35 | /// when given an invalid string.
36 | /// The error should be of type `ValidationError::InvalidAmbiguousTimeStrategy`.
37 | /// expect: `ValidationError::InvalidAmbiguousTimeStrategy`
38 | #[test]
39 | fn test_invalid_ambiguous_time_strategy() {
40 | let res: Result =
41 | validate_string_for_ambiguous_time_strategy("invalid");
42 |
43 | // check status is error
44 | assert!(res.is_err());
45 |
46 | // confirm error type
47 | assert_eq!(
48 | res.unwrap_err(),
49 | ValidationError::AmbiguousTimeStrategy {
50 | ambiguous_time_strategy: "invalid".to_string()
51 | }
52 | );
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/validator/command_options_validator.rs:
--------------------------------------------------------------------------------
1 | use super::native_datetime_validator::validate_string_for_native_datetime;
2 | use super::timezone_validator::validate_string_for_timezone;
3 | use crate::command::validated_options::ambiguous_time_strategy::AmbiguousTimeStrategy;
4 | use crate::command::validated_options::validated_user_inputs::ValidatedCommandOptions;
5 | use crate::validator::ambiguous_time_strategy_validator::validate_string_for_ambiguous_time_strategy;
6 | use crate::validator::validation_error::ValidationError;
7 | use chrono::NaiveDateTime;
8 | use chrono_tz::Tz;
9 | use clap::ArgMatches;
10 |
11 | pub(crate) fn validate_command_options(
12 | arg: &ArgMatches,
13 | ) -> Result {
14 | // arg.get_one::("time") returns Option<&String>, but clap validates the required option
15 | // thus, we can safely unwrap the value
16 | let time_str: &String = arg.get_one::("time").unwrap();
17 | let time_validated: NaiveDateTime = validate_string_for_native_datetime(time_str)?;
18 |
19 | // arg.get_one::("from_timezone") returns Option<&String>, but clap validates the required option
20 | // thus, we can safely unwrap the value
21 | let from_tz_str: &String = arg.get_one::("from_timezone").unwrap();
22 | let from_tz_validated: Tz = validate_string_for_timezone(from_tz_str)?;
23 |
24 | // arg.get_one::("to_timezone") returns Option<&String>, but clap validates the required option
25 | // thus, we can safely unwrap the value
26 | let to_tz_str: &String = arg.get_one::("to_timezone").unwrap();
27 | let to_tz_validated: Tz = validate_string_for_timezone(to_tz_str)?;
28 |
29 | // arg.get_one::("ambiguous_time_strategy") returns Option<&String>, but clap set the default value
30 | // thus, we can safely unwrap the value
31 | let ambiguous_time_strategy_str: &String =
32 | arg.get_one::("ambiguous_time_strategy").unwrap();
33 | let ambiguous_time_strategy_validated: AmbiguousTimeStrategy =
34 | validate_string_for_ambiguous_time_strategy(ambiguous_time_strategy_str)?;
35 |
36 | // Return validated validated_options
37 | Ok(ValidatedCommandOptions::new(
38 | time_validated,
39 | from_tz_validated,
40 | to_tz_validated,
41 | ambiguous_time_strategy_validated,
42 | ))
43 | }
44 |
45 | #[cfg(test)]
46 | mod tests {
47 | use super::*;
48 | use crate::validator::validation_error::ValidationError;
49 | use chrono::NaiveDateTime;
50 | use chrono_tz::Tz;
51 | use clap::{Arg, ArgMatches, Command};
52 |
53 | // Helper function to create ArgMatches for testing
54 | fn create_arg_matches(time: &str, from_tz: &str, to_tz: &str) -> ArgMatches {
55 | Command::new("test")
56 | .arg(Arg::new("time").required(true))
57 | .arg(Arg::new("from_timezone").required(true))
58 | .arg(Arg::new("to_timezone").required(true))
59 | .arg(Arg::new("ambiguous_time_strategy").default_value("earliest"))
60 | .get_matches_from(vec!["test", time, from_tz, to_tz])
61 | }
62 |
63 | /// Test that valid command validated_options are valid
64 | /// expected: `Ok(ValidatedCommandOptions)`
65 | #[test]
66 | fn test_validate_command_options_valid() {
67 | let matches: ArgMatches =
68 | create_arg_matches("2024-06-27 12:34:56", "America/New_York", "Europe/London");
69 |
70 | // Confirm that the validation passes
71 | let result: Result =
72 | validate_command_options(&matches);
73 | assert!(result.is_ok());
74 |
75 | // Confirm that the validated validated_options are as expected
76 | let validated_options: ValidatedCommandOptions = result.unwrap();
77 | assert_eq!(
78 | validated_options.get_param_time(),
79 | NaiveDateTime::parse_from_str("2024-06-27 12:34:56", "%Y-%m-%d %H:%M:%S").unwrap()
80 | );
81 | assert_eq!(
82 | validated_options.get_param_from_tz(),
83 | "America/New_York".parse::().unwrap()
84 | );
85 | assert_eq!(
86 | validated_options.get_param_to_tz(),
87 | "Europe/London".parse::().unwrap()
88 | );
89 | }
90 |
91 | /// Test that an invalid time is invalid
92 | /// expected: `Err(ValidationError::InvalidTime)`
93 | #[test]
94 | fn test_validate_command_options_invalid_time() {
95 | let matches: ArgMatches =
96 | create_arg_matches("invalid-time", "America/New_York", "Europe/London");
97 | let result: Result =
98 | validate_command_options(&matches);
99 | assert!(result.is_err());
100 | }
101 |
102 | /// Test that an invalid from timezone is invalid
103 | /// expected: `Err(ValidationError::InvalidTimezone)`
104 | #[test]
105 | fn test_validate_command_options_invalid_from_timezone() {
106 | let matches: ArgMatches =
107 | create_arg_matches("2024-06-27 12:34:56", "Invalid/Timezone", "Europe/London");
108 | let result: Result =
109 | validate_command_options(&matches);
110 | assert!(result.is_err());
111 | }
112 |
113 | /// Test that an invalid to timezone is invalid
114 | /// expected: `Err(ValidationError::InvalidTimezone)`
115 | #[test]
116 | fn test_validate_command_options_invalid_to_timezone() {
117 | let matches: ArgMatches = create_arg_matches(
118 | "2024-06-27 12:34:56",
119 | "America/New_York",
120 | "Invalid/Timezone",
121 | );
122 | let result: Result =
123 | validate_command_options(&matches);
124 | assert!(result.is_err());
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/validator/native_datetime_validator.rs:
--------------------------------------------------------------------------------
1 | use super::regex_matcher::ymd_hms_matcher::ymd_hms_matcher;
2 | use super::validation_error::ValidationError;
3 | use crate::validator::regex_matcher::ymd_matcher::ymd_matcher;
4 | use crate::validator::regex_matcher::ymd_t_hms_matcher::ymd_t_hms_matcher;
5 | use chrono::NaiveDateTime;
6 |
7 | pub(super) fn validate_string_for_native_datetime(
8 | time: &str,
9 | ) -> Result {
10 | if ymd_hms_matcher(time) {
11 | return NaiveDateTime::parse_from_str(time, "%Y-%m-%d %H:%M:%S")
12 | .map_err(|_| ValidationError::TimeFormat(time.to_string()));
13 | }
14 |
15 | if ymd_matcher(time) {
16 | return NaiveDateTime::parse_from_str(
17 | format!("{} 00:00:00", time).as_str(),
18 | "%Y-%m-%d %H:%M:%S",
19 | )
20 | .map_err(|_| ValidationError::TimeFormat(time.to_string()));
21 | }
22 |
23 | if ymd_t_hms_matcher(time) {
24 | return NaiveDateTime::parse_from_str(time, "%Y-%m-%dT%H:%M:%S")
25 | .map_err(|_| ValidationError::TimeFormat(time.to_string()));
26 | }
27 |
28 | Err(ValidationError::TimeFormat(time.to_string()))
29 | }
30 |
31 | #[cfg(test)]
32 | mod tests {
33 | use super::*;
34 | use chrono::NaiveDateTime;
35 |
36 | /// Test that a valid time string is valid
37 | /// expected: `Ok(NaiveDateTime)`
38 | #[test]
39 | fn test_accept_ym_with_hyphen_hms() {
40 | // Check that a valid time string pass the validator
41 | let time_str = "2024-06-27 12:34:56";
42 | let result: Result =
43 | validate_string_for_native_datetime(time_str);
44 | assert!(result.is_ok());
45 |
46 | // Check that the result value is the same as the expected
47 | let expected_datetime =
48 | NaiveDateTime::parse_from_str(time_str, "%Y-%m-%d %H:%M:%S").unwrap();
49 | assert_eq!(result.unwrap(), expected_datetime);
50 | }
51 |
52 | /// Test that a partial string is valid
53 | /// expected: `Ok(NaiveDateTime)`
54 | #[test]
55 | fn test_accept_ymd_with_hyphen() {
56 | // Check that a valid time string pass the validator
57 | let time_str = "2024-06-27";
58 | let result: Result =
59 | validate_string_for_native_datetime(time_str);
60 | assert!(result.is_ok());
61 |
62 | // Check that the result value is the same as the expected
63 | let expected_datetime = NaiveDateTime::parse_from_str(
64 | format!("{} 00:00:00", time_str).as_str(),
65 | "%Y-%m-%d %H:%M:%S",
66 | )
67 | .unwrap();
68 | assert_eq!(result.unwrap(), expected_datetime);
69 | }
70 |
71 | /// Test that a valid time string is valid
72 | /// we expect the format to be "%Y-%m-%d %H:%M:%S" and "%Y-%m-%dT%H:%M:%S"
73 | /// expected: `Ok(NaiveDateTime)`
74 | #[test]
75 | fn test_accept_iso_format() {
76 | // Check that a valid time string pass the validator
77 | let time_str = "2024-06-27T12:34:56";
78 | let result: Result =
79 | validate_string_for_native_datetime(time_str);
80 | assert!(result.is_ok());
81 |
82 | // Check that the result value is the same as the expected
83 | let expected_datetime =
84 | NaiveDateTime::parse_from_str(time_str, "%Y-%m-%dT%H:%M:%S").unwrap();
85 | assert_eq!(result.unwrap(), expected_datetime);
86 | }
87 |
88 | /// Test that an empty string is invalid
89 | /// expected: `Err(ValidationError::InvalidTimeFormat)`
90 | #[test]
91 | fn test_validate_string_for_native_datetime_empty_string() {
92 | let time_str = "";
93 | let result: Result =
94 | validate_string_for_native_datetime(time_str);
95 | assert!(result.is_err());
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/validator/regex_matcher.rs:
--------------------------------------------------------------------------------
1 | pub(super) mod ymd_hms_matcher;
2 | pub(super) mod ymd_matcher;
3 | pub(super) mod ymd_t_hms_matcher;
4 |
--------------------------------------------------------------------------------
/src/validator/regex_matcher/ymd_hms_matcher.rs:
--------------------------------------------------------------------------------
1 | pub(crate) fn ymd_hms_matcher(text: &str) -> bool {
2 | regex::Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$")
3 | .unwrap()
4 | .is_match(text)
5 | }
6 |
7 | #[cfg(test)]
8 | mod tests {
9 | use super::*;
10 |
11 | /// Test that a valid time string is valid
12 | /// expected: `true`
13 | #[test]
14 | fn test_ymd_hms_matcher_valid() {
15 | // Check that a valid time string pass the validator
16 | let time_str = "2024-06-27 12:34:56";
17 | assert!(ymd_hms_matcher(time_str));
18 | }
19 |
20 | /// Test that an invalid format is invalid
21 | /// we expect the format to be "%Y-%m-%d %H:%M:%S"
22 | /// expected: `false`
23 | #[test]
24 | fn test_ymd_hms_matcher_invalid_format() {
25 | let time_str = "2024-06-27T12:34:56";
26 | assert!(!ymd_hms_matcher(time_str));
27 | }
28 |
29 | /// Test that an empty string is invalid
30 | /// expected: `false`
31 | #[test]
32 | fn test_ymd_hms_matcher_empty_string() {
33 | let time_str = "";
34 | assert!(!ymd_hms_matcher(time_str));
35 | }
36 |
37 | /// Test that a partial string is invalid
38 | /// expected: `false`
39 | #[test]
40 | fn test_ymd_hms_matcher_partial_string() {
41 | let time_str = "2024-06-27";
42 | assert!(!ymd_hms_matcher(time_str));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/validator/regex_matcher/ymd_matcher.rs:
--------------------------------------------------------------------------------
1 | pub(crate) fn ymd_matcher(text: &str) -> bool {
2 | regex::Regex::new(r"^\d{4}-\d{2}-\d{2}$")
3 | .unwrap()
4 | .is_match(text)
5 | }
6 |
7 | #[cfg(test)]
8 | mod tests {
9 | use super::*;
10 |
11 | /// Test that a valid time string is valid
12 | /// expected: `true`
13 | #[test]
14 | fn test_ymd_matcher_valid() {
15 | // Check that a valid time string pass the validator
16 | let time_str = "2024-06-27";
17 | assert!(ymd_matcher(time_str));
18 | }
19 |
20 | /// Test that an invalid format is invalid
21 | /// we expect the format to be "%Y-%m-%d"
22 | /// expected: `false`
23 | #[test]
24 | fn test_ymd_matcher_invalid_format() {
25 | let time_str = "2024-06-27T12:34:56";
26 | assert!(!ymd_matcher(time_str));
27 | }
28 |
29 | /// Test that an empty string is invalid
30 | /// expected: `false`
31 | #[test]
32 | fn test_ymd_matcher_empty_string() {
33 | let time_str = "";
34 | assert!(!ymd_matcher(time_str));
35 | }
36 |
37 | /// Test that a partial string is invalid
38 | /// expected: `false`
39 | #[test]
40 | fn test_ymd_matcher_partial_string() {
41 | let time_str = "2024-06";
42 | assert!(!ymd_matcher(time_str));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/validator/regex_matcher/ymd_t_hms_matcher.rs:
--------------------------------------------------------------------------------
1 | pub(crate) fn ymd_t_hms_matcher(text: &str) -> bool {
2 | regex::Regex::new(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$")
3 | .unwrap()
4 | .is_match(text)
5 | }
6 |
7 | #[cfg(test)]
8 | mod tests {
9 | use super::*;
10 |
11 | /// Test that a valid time string is valid
12 | /// expected: `true`
13 | #[test]
14 | fn test_ymd_t_hms_matcher_valid() {
15 | // Check that a valid time string pass the validator
16 | let time_str = "2024-06-27T12:34:56";
17 | assert!(ymd_t_hms_matcher(time_str));
18 | }
19 |
20 | /// Test that an invalid format is invalid
21 | /// we expect the format to be "%Y-%m-%d %H:%M:%S"
22 | /// expected: `false`
23 | #[test]
24 | fn test_ymd_t_hms_matcher_invalid_format() {
25 | let time_str = "2024-06-27 12:34:56";
26 | assert!(!ymd_t_hms_matcher(time_str));
27 | }
28 |
29 | /// Test that an empty string is invalid
30 | /// expected: `false`
31 | #[test]
32 | fn test_ymd_t_hms_matcher_empty_string() {
33 | let time_str = "";
34 | assert!(!ymd_t_hms_matcher(time_str));
35 | }
36 |
37 | /// Test that a partial string is invalid
38 | /// expected: `false`
39 | #[test]
40 | fn test_ymd_t_hms_matcher_partial_string() {
41 | let time_str = "2024-06-27";
42 | assert!(!ymd_t_hms_matcher(time_str));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/validator/timezone_validator.rs:
--------------------------------------------------------------------------------
1 | use super::validation_error::ValidationError;
2 | use chrono_tz::Tz;
3 |
4 | pub(super) fn validate_string_for_timezone(tz: &str) -> Result {
5 | tz.parse::()
6 | .map_err(|_| ValidationError::Timezone(tz.to_string()))
7 | }
8 |
9 | #[cfg(test)]
10 | mod tests {
11 | use super::*;
12 | use chrono_tz::Tz;
13 |
14 | /// Test that a valid timezone string is valid
15 | /// expected: Ok(Tz)
16 | #[test]
17 | fn test_validate_string_for_timezone_valid() {
18 | // confirm that a valid timezone string passes the validator
19 | let tz_str = "America/New_York";
20 | let result: Result = validate_string_for_timezone(tz_str);
21 | assert!(result.is_ok());
22 |
23 | // confirm that the result value is the same as the expected
24 | let expected_tz: Tz = tz_str.parse().unwrap();
25 | assert_eq!(result.unwrap(), expected_tz);
26 | }
27 |
28 | /// Test that an invalid timezone is invalid
29 | /// expected: `Err(ValidationError::InvalidTimezone)`
30 | #[test]
31 | fn test_validate_string_for_timezone_invalid() {
32 | let tz_str = "Invalid/Timezone";
33 | let result: Result = validate_string_for_timezone(tz_str);
34 | assert!(result.is_err());
35 | }
36 |
37 | /// Test that an empty string is invalid
38 | /// expected: `Err(ValidationError::InvalidTimezone)`
39 | #[test]
40 | fn test_validate_string_for_timezone_empty_string() {
41 | let tz_str = "";
42 | let result: Result = validate_string_for_timezone(tz_str);
43 | assert!(result.is_err());
44 | }
45 |
46 | /// Test that a partial string is invalid
47 | /// expected: `Err(ValidationError::InvalidTimezone)`
48 | #[test]
49 | fn test_validate_string_for_timezone_partial_string() {
50 | let tz_str = "America";
51 | let result: Result = validate_string_for_timezone(tz_str);
52 | assert!(result.is_err());
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/validator/validation_error.rs:
--------------------------------------------------------------------------------
1 | #[derive(thiserror::Error, Debug, PartialEq)]
2 | pub(crate) enum ValidationError {
3 | #[error("Validation Error: Invalid time format found. {0} (expected: YYYY-MM-DD hh:mm:ss)")]
4 | TimeFormat(String),
5 |
6 | #[error(
7 | "Validation Error: Invalid timezone found {0}. @see https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html"
8 | )]
9 | Timezone(String),
10 |
11 | #[error("Validation Error: Invalid ambiguous time strategy found. {ambiguous_time_strategy} (expected: earliest, latest)")]
12 | AmbiguousTimeStrategy { ambiguous_time_strategy: String },
13 | }
14 |
--------------------------------------------------------------------------------