From 35f58dedc8a994da4d666f4cf2a2fa4373778c31 Mon Sep 17 00:00:00 2001 From: "newton@alisx.com" Date: Tue, 15 Oct 2024 13:15:48 +0300 Subject: [PATCH] feat(): Initial commit --- .gitignore | 6 + .vscode/settings.json | 4 + README.md | 7 + build_npm.ts | 28 ++++ deno.json | 15 ++ deno.lock | 136 ++++++++++++++++ mod.ts | 6 + money/index.ts | 66 ++++++++ money/index_test.ts | 121 ++++++++++++++ numbers/index.ts | 24 +++ numbers/index_test.ts | 15 ++ strings/index.ts | 206 ++++++++++++++++++++++++ strings/index_test.ts | 367 ++++++++++++++++++++++++++++++++++++++++++ time/index.ts | 176 ++++++++++++++++++++ time/index_test.ts | 174 ++++++++++++++++++++ 15 files changed, 1351 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 build_npm.ts create mode 100644 deno.json create mode 100644 deno.lock create mode 100644 mod.ts create mode 100644 money/index.ts create mode 100644 money/index_test.ts create mode 100644 numbers/index.ts create mode 100644 numbers/index_test.ts create mode 100644 strings/index.ts create mode 100644 strings/index_test.ts create mode 100644 time/index.ts create mode 100644 time/index_test.ts diff --git a/.gitignore b/.gitignore index c6bba59..8c7d0fe 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,9 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# Deno files +npm/ + +# VSCode +.vscode/launch.json \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..352db03 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "deno.enable": true, + "deno.lint": true, +} \ No newline at end of file diff --git a/README.md b/README.md index d64ce2c..9122b0a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,9 @@ # ts-alis-build + Alis Build utils for Javascript/Typescript + +## Installation + +```bash +npm install @alis-build/utils +``` \ No newline at end of file diff --git a/build_npm.ts b/build_npm.ts new file mode 100644 index 0000000..2531863 --- /dev/null +++ b/build_npm.ts @@ -0,0 +1,28 @@ +import { build, emptyDir } from "https://deno.land/x/dnt@0.37.0/mod.ts"; + +await emptyDir("./npm"); + +await build({ + entryPoints: ["./mod.ts"], + outDir: "./npm", + shims: { + deno: true, + }, + package: { + name: "@alis-build/utils", + version: Deno.args[0], + description: "A collection of common utilities used at Alis Exchange.", + license: "APACHE-2.0", + repository: { + type: "git", + url: "git+https://github.com/alis-exchange/ts-alis-build.git", + }, + bugs: { + url: "https://github.com/alis-exchange/ts-alis-build/issues", + }, + }, + postBuild() { + Deno.copyFileSync("LICENSE", "npm/LICENSE"); + Deno.copyFileSync("README.md", "npm/README.md"); + }, +}); diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..087b981 --- /dev/null +++ b/deno.json @@ -0,0 +1,15 @@ +{ + "name": "@alis-build/utils", + "version": "0.1.0", + "exports": "./mod.ts", + "tasks": { + "dev": "deno test --watch mod.ts" + }, + "license": "MIT", + "imports": { + "@alis-build/google-common-protos": "npm:@alis-build/google-common-protos@^1.7.0", + "@std/assert": "jsr:@std/assert@^1.0.6", + "google-protobuf": "npm:google-protobuf@^3.21.4" + }, + "nodeModulesDir": "auto" +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..a20803c --- /dev/null +++ b/deno.lock @@ -0,0 +1,136 @@ +{ + "version": "4", + "specifiers": { + "jsr:@std/assert@^1.0.6": "1.0.6", + "jsr:@std/internal@^1.0.4": "1.0.4", + "npm:@alis-build/google-common-protos@*": "1.7.0", + "npm:@alis-build/google-common-protos@^1.7.0": "1.7.0", + "npm:@alis-build/google-common-protos@latest": "1.7.0", + "npm:google-protobuf@*": "3.21.4", + "npm:google-protobuf@^3.21.4": "3.21.4" + }, + "jsr": { + "@std/assert@1.0.6": { + "integrity": "1904c05806a25d94fe791d6d883b685c9e2dcd60e4f9fc30f4fc5cf010c72207", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/internal@1.0.4": { + "integrity": "62e8e4911527e5e4f307741a795c0b0a9e6958d0b3790716ae71ce085f755422" + } + }, + "npm": { + "@alis-build/google-common-protos@1.7.0": { + "integrity": "sha512-HYyAJKqsgoqmjq+XUrC60IgIL+IizY14stNeJ0+6InmSDpDCAjzoDcRp2C2vNJ61Trl1vRm9ONvEcssZfJksuQ==" + }, + "google-protobuf@3.21.4": { + "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==" + } + }, + "remote": { + "https://deno.land/std@0.140.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", + "https://deno.land/std@0.140.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49", + "https://deno.land/std@0.140.0/bytes/bytes_list.ts": "67eb118e0b7891d2f389dad4add35856f4ad5faab46318ff99653456c23b025d", + "https://deno.land/std@0.140.0/bytes/equals.ts": "fc16dff2090cced02497f16483de123dfa91e591029f985029193dfaa9d894c9", + "https://deno.land/std@0.140.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf", + "https://deno.land/std@0.140.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37", + "https://deno.land/std@0.140.0/fs/_util.ts": "0fb24eb4bfebc2c194fb1afdb42b9c3dda12e368f43e8f2321f84fc77d42cb0f", + "https://deno.land/std@0.140.0/fs/ensure_dir.ts": "9dc109c27df4098b9fc12d949612ae5c9c7169507660dcf9ad90631833209d9d", + "https://deno.land/std@0.140.0/hash/sha256.ts": "803846c7a5a8a5a97f31defeb37d72f519086c880837129934f5d6f72102a8e8", + "https://deno.land/std@0.140.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b", + "https://deno.land/std@0.140.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", + "https://deno.land/std@0.140.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", + "https://deno.land/std@0.140.0/path/_util.ts": "c1e9686d0164e29f7d880b2158971d805b6e0efc3110d0b3e24e4b8af2190d2b", + "https://deno.land/std@0.140.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", + "https://deno.land/std@0.140.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee", + "https://deno.land/std@0.140.0/path/mod.ts": "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d", + "https://deno.land/std@0.140.0/path/posix.ts": "293cdaec3ecccec0a9cc2b534302dfe308adb6f10861fa183275d6695faace44", + "https://deno.land/std@0.140.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", + "https://deno.land/std@0.140.0/path/win32.ts": "31811536855e19ba37a999cd8d1b62078235548d67902ece4aa6b814596dd757", + "https://deno.land/std@0.140.0/streams/conversion.ts": "712585bfa0172a97fb68dd46e784ae8ad59d11b88079d6a4ab098ff42e697d21", + "https://deno.land/std@0.181.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", + "https://deno.land/std@0.181.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", + "https://deno.land/std@0.181.0/fs/_util.ts": "65381f341af1ff7f40198cee15c20f59951ac26e51ddc651c5293e24f9ce6f32", + "https://deno.land/std@0.181.0/fs/ensure_dir.ts": "dc64c4c75c64721d4e3fb681f1382f803ff3d2868f08563ff923fdd20d071c40", + "https://deno.land/std@0.181.0/fs/expand_glob.ts": "e4f56259a0a70fe23f05215b00de3ac5e6ba46646ab2a06ebbe9b010f81c972a", + "https://deno.land/std@0.181.0/fs/walk.ts": "ea95ffa6500c1eda6b365be488c056edc7c883a1db41ef46ec3bf057b1c0fe32", + "https://deno.land/std@0.181.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", + "https://deno.land/std@0.181.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", + "https://deno.land/std@0.181.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", + "https://deno.land/std@0.181.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", + "https://deno.land/std@0.181.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", + "https://deno.land/std@0.181.0/path/mod.ts": "bf718f19a4fdd545aee1b06409ca0805bd1b68ecf876605ce632e932fe54510c", + "https://deno.land/std@0.181.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", + "https://deno.land/std@0.181.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", + "https://deno.land/std@0.181.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", + "https://deno.land/std@0.182.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", + "https://deno.land/std@0.182.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", + "https://deno.land/std@0.182.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e", + "https://deno.land/std@0.182.0/fs/_util.ts": "65381f341af1ff7f40198cee15c20f59951ac26e51ddc651c5293e24f9ce6f32", + "https://deno.land/std@0.182.0/fs/empty_dir.ts": "c3d2da4c7352fab1cf144a1ecfef58090769e8af633678e0f3fabaef98594688", + "https://deno.land/std@0.182.0/fs/expand_glob.ts": "e4f56259a0a70fe23f05215b00de3ac5e6ba46646ab2a06ebbe9b010f81c972a", + "https://deno.land/std@0.182.0/fs/walk.ts": "920be35a7376db6c0b5b1caf1486fb962925e38c9825f90367f8f26b5e5d0897", + "https://deno.land/std@0.182.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", + "https://deno.land/std@0.182.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", + "https://deno.land/std@0.182.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", + "https://deno.land/std@0.182.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", + "https://deno.land/std@0.182.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", + "https://deno.land/std@0.182.0/path/mod.ts": "bf718f19a4fdd545aee1b06409ca0805bd1b68ecf876605ce632e932fe54510c", + "https://deno.land/std@0.182.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", + "https://deno.land/std@0.182.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", + "https://deno.land/std@0.182.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", + "https://deno.land/std@0.192.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e", + "https://deno.land/std@0.192.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea", + "https://deno.land/std@0.192.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", + "https://deno.land/std@0.192.0/testing/asserts.ts": "e16d98b4d73ffc4ed498d717307a12500ae4f2cbe668f1a215632d19fcffc22f", + "https://deno.land/x/code_block_writer@12.0.0/mod.ts": "2c3448060e47c9d08604c8f40dee34343f553f33edcdfebbf648442be33205e5", + "https://deno.land/x/code_block_writer@12.0.0/utils/string_utils.ts": "60cb4ec8bd335bf241ef785ccec51e809d576ff8e8d29da43d2273b69ce2a6ff", + "https://deno.land/x/deno_cache@0.4.1/auth_tokens.ts": "5fee7e9155e78cedf3f6ff3efacffdb76ac1a76c86978658d9066d4fb0f7326e", + "https://deno.land/x/deno_cache@0.4.1/cache.ts": "51f72f4299411193d780faac8c09d4e8cbee951f541121ef75fcc0e94e64c195", + "https://deno.land/x/deno_cache@0.4.1/deno_dir.ts": "f2a9044ce8c7fe1109004cda6be96bf98b08f478ce77e7a07f866eff1bdd933f", + "https://deno.land/x/deno_cache@0.4.1/deps.ts": "8974097d6c17e65d9a82d39377ae8af7d94d74c25c0cbb5855d2920e063f2343", + "https://deno.land/x/deno_cache@0.4.1/dirs.ts": "d2fa473ef490a74f2dcb5abb4b9ab92a48d2b5b6320875df2dee64851fa64aa9", + "https://deno.land/x/deno_cache@0.4.1/disk_cache.ts": "1f3f5232cba4c56412d93bdb324c624e95d5dd179d0578d2121e3ccdf55539f9", + "https://deno.land/x/deno_cache@0.4.1/file_fetcher.ts": "07a6c5f8fd94bf50a116278cc6012b4921c70d2251d98ce1c9f3c352135c39f7", + "https://deno.land/x/deno_cache@0.4.1/http_cache.ts": "f632e0d6ec4a5d61ae3987737a72caf5fcdb93670d21032ddb78df41131360cd", + "https://deno.land/x/deno_cache@0.4.1/mod.ts": "ef1cda9235a93b89cb175fe648372fc0f785add2a43aa29126567a05e3e36195", + "https://deno.land/x/deno_cache@0.4.1/util.ts": "8cb686526f4be5205b92c819ca2ce82220aa0a8dd3613ef0913f6dc269dbbcfe", + "https://deno.land/x/deno_graph@0.26.0/lib/deno_graph.generated.js": "2f7ca85b2ceb80ec4b3d1b7f3a504956083258610c7b9a1246238c5b7c68f62d", + "https://deno.land/x/deno_graph@0.26.0/lib/loader.ts": "380e37e71d0649eb50176a9786795988fc3c47063a520a54b616d7727b0f8629", + "https://deno.land/x/deno_graph@0.26.0/lib/media_type.ts": "222626d524fa2f9ebcc0ec7c7a7d5dfc74cc401cc46790f7c5e0eab0b0787707", + "https://deno.land/x/deno_graph@0.26.0/lib/snippets/deno_graph-de651bc9c240ed8d/src/deno_apis.js": "41192baaa550a5c6a146280fae358cede917ae16ec4e4315be51bef6631ca892", + "https://deno.land/x/deno_graph@0.26.0/mod.ts": "11131ae166580a1c7fa8506ff553751465a81c263d94443f18f353d0c320bc14", + "https://deno.land/x/dir@1.5.1/data_local_dir/mod.ts": "91eb1c4bfadfbeda30171007bac6d85aadacd43224a5ed721bbe56bc64e9eb66", + "https://deno.land/x/dnt@0.37.0/lib/compiler.ts": "209ad2e1b294f93f87ec02ade9a0821f942d2e524104552d0aa8ff87021050a5", + "https://deno.land/x/dnt@0.37.0/lib/compiler_transforms.ts": "cbb1fd5948f5ced1aa5c5aed9e45134e2357ce1e7220924c1d7bded30dcd0dd0", + "https://deno.land/x/dnt@0.37.0/lib/mod.deps.ts": "30367fc68bcd2acf3b7020cf5cdd26f817f7ac9ac35c4bfb6c4551475f91bc3e", + "https://deno.land/x/dnt@0.37.0/lib/npm_ignore.ts": "b430caa1905b65ae89b119d84857b3ccc3cb783a53fc083d1970e442f791721d", + "https://deno.land/x/dnt@0.37.0/lib/package_json.ts": "61f35b06e374ed39ca776d29d67df4be7ee809d0bca29a8239687556c6d027c2", + "https://deno.land/x/dnt@0.37.0/lib/pkg/dnt_wasm.generated.js": "65514d733c044bb394e4765321e33b73c490b20f86563293b5665d7a7b185153", + "https://deno.land/x/dnt@0.37.0/lib/pkg/snippets/dnt-wasm-a15ef721fa5290c5/helpers.js": "a6b95adc943a68d513fe8ed9ec7d260ac466b7a4bced4e942f733e494bb9f1be", + "https://deno.land/x/dnt@0.37.0/lib/shims.ts": "df1bd4d9a196dca4b2d512b1564fff64ac6c945189a273d706391f87f210d7e6", + "https://deno.land/x/dnt@0.37.0/lib/test_runner/get_test_runner_code.ts": "4dc7a73a13b027341c0688df2b29a4ef102f287c126f134c33f69f0339b46968", + "https://deno.land/x/dnt@0.37.0/lib/test_runner/test_runner.ts": "4d0da0500ec427d5f390d9a8d42fb882fbeccc92c92d66b6f2e758606dbd40e6", + "https://deno.land/x/dnt@0.37.0/lib/transform.deps.ts": "e42f2bdef46d098453bdba19261a67cf90b583f5d868f7fe83113c1380d9b85c", + "https://deno.land/x/dnt@0.37.0/lib/types.ts": "b8e228b2fac44c2ae902fbb73b1689f6ab889915bd66486c8a85c0c24255f5fb", + "https://deno.land/x/dnt@0.37.0/lib/utils.ts": "878b7ac7003a10c16e6061aa49dbef9b42bd43174853ebffc9b67ea47eeb11d8", + "https://deno.land/x/dnt@0.37.0/mod.ts": "37d0c784371cf1750f30203a95de2555ba4c1aa89d826024f14c038f87e0f344", + "https://deno.land/x/dnt@0.37.0/transform.ts": "1b127c5f22699c8ab2545b98aeca38c4e5c21405b0f5342ea17e9c46280ed277", + "https://deno.land/x/ts_morph@18.0.0/bootstrap/mod.ts": "b53aad517f106c4079971fcd4a81ab79fadc40b50061a3ab2b741a09119d51e9", + "https://deno.land/x/ts_morph@18.0.0/bootstrap/ts_morph_bootstrap.js": "6645ac03c5e6687dfa8c78109dc5df0250b811ecb3aea2d97c504c35e8401c06", + "https://deno.land/x/ts_morph@18.0.0/common/DenoRuntime.ts": "6a7180f0c6e90dcf23ccffc86aa8271c20b1c4f34c570588d08a45880b7e172d", + "https://deno.land/x/ts_morph@18.0.0/common/mod.ts": "01985d2ee7da8d1caee318a9d07664774fbee4e31602bc2bb6bb62c3489555ed", + "https://deno.land/x/ts_morph@18.0.0/common/ts_morph_common.js": "845671ca951073400ce142f8acefa2d39ea9a51e29ca80928642f3f8cf2b7700", + "https://deno.land/x/ts_morph@18.0.0/common/typescript.js": "d5c598b6a2db2202d0428fca5fd79fc9a301a71880831a805d778797d2413c59", + "https://deno.land/x/wasmbuild@0.13.0/cache.ts": "89eea5f3ce6035a1164b3e655c95f21300498920575ade23161421f5b01967f4", + "https://deno.land/x/wasmbuild@0.13.0/loader.ts": "d98d195a715f823151cbc8baa3f32127337628379a02d9eb2a3c5902dbccfc02" + }, + "workspace": { + "dependencies": [ + "jsr:@std/assert@^1.0.6", + "npm:@alis-build/google-common-protos@^1.7.0", + "npm:google-protobuf@^3.21.4" + ] + } +} diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..ac0aee7 --- /dev/null +++ b/mod.ts @@ -0,0 +1,6 @@ +import * as money from "./money/index.ts"; +import * as strings from "./strings/index.ts"; +import * as time from "./time/index.ts"; +import * as numbers from "./numbers/index.ts"; + +export { money, strings, time, numbers }; diff --git a/money/index.ts b/money/index.ts new file mode 100644 index 0000000..b2b8be1 --- /dev/null +++ b/money/index.ts @@ -0,0 +1,66 @@ +import Money from "npm:@alis-build/google-common-protos@latest/google/type/money_pb.js"; +import { modf } from "../numbers/index.ts"; + +/** + * Parse a google.type.Money instance into a number + * + * @param money {google.type.Money} - The money to parse + * @returns {number | null} The parsed number, or null if the input is nullish + */ +export const parse = (money: Money.Money): number | null => { + // If money is nullish, return null + if (!money) { + return null; + } + + const units = money.getUnits(); + const nanos = money.getNanos(); + + return units + nanos / 1e9; +}; + +/** + * Encode a number into a google.type.Money instance + * + * @param currency {string} - The currency code + * @param value {number} - The number to encode + * @returns {Money | null} The encoded Money instance, or null if the input is nullish + */ +export const encode = (currency: string, value: number): Money.Money | null => { + if (!currency || !value) { + return null; + } + + const [units, nanos] = modf(value); + + const rawNanos = nanos * 1e9; + const absNanosString = rawNanos?.toString()?.split(".")[0]; + const absNanos = absNanosString ? parseInt(absNanosString) : 0; + + const money = new Money.Money(); + money.setCurrencyCode(currency); + money.setUnits(units); + money.setNanos(absNanos); + + return money; +}; + +/** + * Takes a google.type.Money object and returns a string formatted as currency. + * + * @param money {google.type.Money} - The money object to format + * @returns {string} + */ +export const format = (money?: Money.Money): string => { + if (!money) { + return "0.0"; + } + + // Create a JS number formatter + const currencyFormatter = new Intl.NumberFormat("en-US", { + style: "currency", + currency: money.getCurrencyCode(), + }); + + return currencyFormatter.format(parse(money) ?? 0.0); +}; diff --git a/money/index_test.ts b/money/index_test.ts new file mode 100644 index 0000000..49eb991 --- /dev/null +++ b/money/index_test.ts @@ -0,0 +1,121 @@ +import { assertEquals } from "https://deno.land/std@0.192.0/testing/asserts.ts"; +import Money from "npm:@alis-build/google-common-protos@latest/google/type/money_pb.js"; +import { parse, encode, format } from "./index.ts"; + +Deno.test({ + name: "parseMoney", + fn() { + const money = new Money.Money(); + money.setCurrencyCode("USD"); + money.setUnits(100); + money.setNanos(0); + + const parsed = parse(money); + + assertEquals( + parsed, + 100, + `Expected parsed value to be 100, but got ${parsed}` + ); + }, +}); + +Deno.test({ + name: "parseMoneyWithNanos", + fn() { + const money = new Money.Money(); + money.setCurrencyCode("USD"); + money.setUnits(100); + money.setNanos(200000000); + + const parsed = parse(money); + + assertEquals( + parsed, + 100.2, + `Expected parsed value to be 100.20, but got ${parsed}` + ); + }, +}); + +Deno.test({ + name: "encodeMoney", + fn() { + const money = encode("USD", 100); + + assertEquals( + money?.getCurrencyCode(), + "USD", + `Expected currency code to be USD, but got ${money?.getCurrencyCode()}` + ); + assertEquals( + money?.getUnits(), + 100, + `Expected units to be 100, but got ${money?.getUnits()}` + ); + assertEquals( + money?.getNanos(), + 0, + `Expected nanos to be 0, but got ${money?.getNanos()}` + ); + }, +}); + +Deno.test({ + name: "encodeMoneyWithNanos", + fn() { + const money = encode("USD", 100.2); + + assertEquals( + money?.getCurrencyCode(), + "USD", + `Expected currency code to be USD, but got ${money?.getCurrencyCode()}` + ); + assertEquals( + money?.getUnits(), + 100, + `Expected units to be 100, but got ${money?.getUnits()}` + ); + assertEquals( + money?.getNanos(), + 200000000, + `Expected nanos to be 200000000, but got ${money?.getNanos()}` + ); + }, +}); + +Deno.test({ + name: "formatMoney", + fn() { + const money = new Money.Money(); + money.setCurrencyCode("USD"); + money.setUnits(100); + money.setNanos(0); + + const formatted = format(money); + + assertEquals( + formatted, + "$100.00", + `Expected formatted value to be $100.00, but got ${formatted}` + ); + }, +}); + +Deno.test({ + name: "formatMoneyWithNanos", + fn() { + const money = new Money.Money(); + money.setCurrencyCode("USD"); + money.setUnits(100); + money.setNanos(200000000); + + const formatted = format(money); + + assertEquals( + formatted, + "$100.20", + `Expected formatted value to be $100.20, but got ${formatted}` + ); + }, +}); diff --git a/numbers/index.ts b/numbers/index.ts new file mode 100644 index 0000000..9db197e --- /dev/null +++ b/numbers/index.ts @@ -0,0 +1,24 @@ +/** + * Deconstructs a number into its integer and fractional parts. + * Example: + * + * const [units, nanos] = Modf(123.456); + * + * @param value - number to deconstruct + * @returns {number[]} + */ +export const modf = (value: number): [units: number, nanos: number] => { + // return integer part - may be negative + const trunc = () => { + return value < 0 ? Math.ceil(value) : Math.floor(value); + }; + + // return fraction part + const frac = () => { + const nanos = value - Math.trunc(value); + + return value < 0 ? -nanos : nanos; + }; + + return [trunc(), frac()]; +}; diff --git a/numbers/index_test.ts b/numbers/index_test.ts new file mode 100644 index 0000000..af92fff --- /dev/null +++ b/numbers/index_test.ts @@ -0,0 +1,15 @@ +import { assertEquals } from "https://deno.land/std@0.192.0/testing/asserts.ts"; +import { modf } from "./index.ts"; + +Deno.test({ + name: "modf", + fn() { + const [units, nanos] = modf(100.2); + const rawNanos = nanos * 1e9; + const absNanosString = rawNanos?.toString()?.split(".")[0]; + const absNanos = absNanosString ? parseInt(absNanosString) : 0; + + assertEquals(units, 100); + assertEquals(absNanos, 200000000); + }, +}); diff --git a/strings/index.ts b/strings/index.ts new file mode 100644 index 0000000..3b48623 --- /dev/null +++ b/strings/index.ts @@ -0,0 +1,206 @@ +/** + * Converts a string from snake_case to camelCase + * @param str {string} - The string to convert + * @returns {string} The converted string + */ +export const snakeCaseToCamelCase = (str: string): string => { + if (!str) { + return str; + } + + // Strip leading and trailing underscores + str = str.replace(/^_+|_+$/g, ""); + + return str.replace(/_([a-zA-Z])/g, (_, letter) => letter.toUpperCase()); +}; + +/** + * Converts a string from camelCase to snake_case + * @param str {string} - The string to convert + * @returns {string} The converted string + */ +export const camelCaseToSnakeCase = (str: string): string => { + if (!str) { + return str; + } + + return str.replace(/([A-Z])/g, "_$1").toLowerCase(); +}; + +/** + * Converts a string from snakeCase to kebab-case + * @param str {string} - The string to convert + * @returns {string} The converted string + */ +export const snakeCaseToKebabCase = (str: string): string => { + if (!str) { + return str; + } + + return str.replace(/_/g, "-"); +}; + +/** + * Converts a string from kebab-case to snake_case + * @param str {string} - The string to convert + * @returns {string} The converted string + */ +export const kebabCaseToSnakeCase = (str: string): string => { + if (!str) { + return str; + } + + return str.replace(/-/g, "_"); +}; + +/** + * Converts a string from camelCase to kebab-case + * @param str {string} - The string to convert + * @returns {string} The converted string + */ +export const camelCaseToKebabCase = (str: string): string => { + if (!str) { + return str; + } + + return camelCaseToSnakeCase(str).replace(/_/g, "-"); +}; + +/** + * Converts a string from kebab-case to camelCase + * @param str {string} - The string to convert + * @returns {string} The converted string + */ +export const kebabCaseToCamelCase = (str: string): string => { + if (!str) { + return str; + } + + return snakeCaseToCamelCase(kebabCaseToSnakeCase(str)); +}; + +/** + * Converts a string from snake_case to PascalCase + * @param str {string} - The string to convert + * @returns {string} The converted string + */ +export const snakeCaseToPascalCase = (str: string): string => { + if (!str) { + return str; + } + + return snakeCaseToCamelCase(str).replace(/^\w/, (c) => c.toUpperCase()); +}; + +/** + * Converts a string from PascalCase to snake_case + * @param str {string} - The string to convert + * @returns {string} The converted string + */ +export const pascalCaseToSnakeCase = (str: string): string => { + if (!str) { + return str; + } + + return camelCaseToSnakeCase(str).replace(/^_+|_+$/g, ""); +}; + +/** + * Converts a string from PascalCase to camelCase + * @param str {string} - The string to convert + * @returns {string} The converted string + */ +export const kebabCaseToPascalCase = (str: string): string => { + if (!str) { + return str; + } + + return kebabCaseToCamelCase(str).replace(/^\w/, (c) => c.toUpperCase()); +}; + +/** + * Converts a string from PascalCase to kebab-case + * @param str {string} - The string to convert + * @returns {string} The converted string + */ +export const pascalCaseToKebabCase = (str: string): string => { + if (!str) { + return str; + } + + return camelCaseToKebabCase(str).replace(/^-+|-+$/g, ""); +}; + +/** + * Converts a string from camelCase to PascalCase + * @param str {string} - The string to convert + * @returns {string} The converted string + */ +export const camelCaseToPascalCase = (str: string): string => { + if (!str) { + return str; + } + + return str.replace(/^\w/, (c) => c.toUpperCase()); +}; + +/** + * Converts a string from PascalCase to camelCase + * @param str {string} - The string to convert + * @returns {string} The converted string + */ +export const pascalCaseToCamelCase = (str: string): string => { + if (!str) { + return str; + } + + return str.replace(/^\w/, (c) => c.toLowerCase()); +}; + +/** + * Converts a string to Title Case. + * @param str - The string to convert. + * @returns The Title Case version of the input string. + */ +export function toTitleCase(str: string): string { + // Split the string into words + const words = str + // Handle camelCase and PascalCase + .replace(/([a-z0-9])([A-Z])/g, "$1 $2") + // Handle ALLCAPS to capitalize only first letter + .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2") + // Replace underscores and hyphens with spaces + .replace(/[_\-]+/g, " ") + // Convert to lowercase + .toLowerCase() + // Split into words by spaces + .split(/\s+/); + + // Capitalize the first letter of each word + const capitalizedWords = words.map( + (word) => word.charAt(0).toUpperCase() + word.slice(1) + ); + + // Join the words with spaces + return capitalizedWords.join(" "); +} + +/** + * Converts a string to CONSTANT_CASE. + * @param str - The string to convert. + * @returns The CONSTANT_CASE version of the input string. + */ +export function toConstantCase(str: string): string { + // Split the string into words + const words = str + // Handle camelCase and PascalCase + .replace(/([a-z0-9])([A-Z])/g, "$1_$2") + // Handle ALLCAPS acronyms followed by lowercase letters + .replace(/([A-Z])([A-Z][a-z])/g, "$1_$2") + // Replace spaces, hyphens, and underscores with a single underscore + .replace(/[\s\-_]+/g, "_") + // Convert to uppercase + .toUpperCase(); + + return words; +} diff --git a/strings/index_test.ts b/strings/index_test.ts new file mode 100644 index 0000000..0e586d0 --- /dev/null +++ b/strings/index_test.ts @@ -0,0 +1,367 @@ +import { assertEquals } from "https://deno.land/std@0.192.0/testing/asserts.ts"; +import { + snakeCaseToCamelCase, + camelCaseToSnakeCase, + snakeCaseToKebabCase, + kebabCaseToSnakeCase, + camelCaseToKebabCase, + kebabCaseToCamelCase, + snakeCaseToPascalCase, + pascalCaseToSnakeCase, + kebabCaseToPascalCase, + pascalCaseToKebabCase, + camelCaseToPascalCase, + pascalCaseToCamelCase, + toTitleCase, + toConstantCase, +} from "./index.ts"; + +Deno.test({ + name: "snakeCaseToCamelCase", + fn() { + assertEquals(snakeCaseToCamelCase("snake_case"), "snakeCase"); + assertEquals(snakeCaseToCamelCase("snake_Case_test"), "snakeCaseTest"); + assertEquals( + snakeCaseToCamelCase("snake_case_test_test"), + "snakeCaseTestTest" + ); + assertEquals( + snakeCaseToCamelCase("_snake_case_test_test"), + "snakeCaseTestTest" + ); + assertEquals( + snakeCaseToCamelCase("snake_case_test_test_"), + "snakeCaseTestTest" + ); + assertEquals( + snakeCaseToCamelCase("_snake_case_test_test_"), + "snakeCaseTestTest" + ); + assertEquals( + snakeCaseToCamelCase("snakeCaseTestTest"), + "snakeCaseTestTest" + ); + }, +}); + +Deno.test({ + name: "camelCaseToSnakeCase", + fn() { + assertEquals(camelCaseToSnakeCase("camelCase"), "camel_case"); + assertEquals(camelCaseToSnakeCase("camelCaseTest"), "camel_case_test"); + assertEquals( + camelCaseToSnakeCase("camelCaseTestTest"), + "camel_case_test_test" + ); + assertEquals( + camelCaseToSnakeCase("_camelCaseTestTest"), + "_camel_case_test_test" + ); + assertEquals( + camelCaseToSnakeCase("camelCaseTestTest_"), + "camel_case_test_test_" + ); + assertEquals( + camelCaseToSnakeCase("_camelCaseTestTest_"), + "_camel_case_test_test_" + ); + assertEquals( + camelCaseToSnakeCase("camel_case_test_test"), + "camel_case_test_test" + ); + }, +}); + +Deno.test({ + name: "snakeCaseToKebabCase", + fn() { + assertEquals(snakeCaseToKebabCase("snake_case"), "snake-case"); + assertEquals(snakeCaseToKebabCase("snake_Case_test"), "snake-Case-test"); + assertEquals( + snakeCaseToKebabCase("snake_case_test_test"), + "snake-case-test-test" + ); + assertEquals( + snakeCaseToKebabCase("_snake_case_test_test"), + "-snake-case-test-test" + ); + assertEquals( + snakeCaseToKebabCase("snake_case_test_test_"), + "snake-case-test-test-" + ); + assertEquals( + snakeCaseToKebabCase("_snake_case_test_test_"), + "-snake-case-test-test-" + ); + assertEquals( + snakeCaseToKebabCase("snakeCaseTestTest"), + "snakeCaseTestTest" + ); + }, +}); + +Deno.test({ + name: "kebabCaseToSnakeCase", + fn() { + assertEquals(kebabCaseToSnakeCase("kebab-case"), "kebab_case"); + assertEquals(kebabCaseToSnakeCase("kebab-Case-test"), "kebab_Case_test"); + assertEquals( + kebabCaseToSnakeCase("kebab-case-test-test"), + "kebab_case_test_test" + ); + assertEquals( + kebabCaseToSnakeCase("-kebab-case-test-test"), + "_kebab_case_test_test" + ); + assertEquals( + kebabCaseToSnakeCase("kebab-case-test-test-"), + "kebab_case_test_test_" + ); + assertEquals( + kebabCaseToSnakeCase("-kebab-case-test-test-"), + "_kebab_case_test_test_" + ); + assertEquals( + kebabCaseToSnakeCase("kebabCaseTestTest"), + "kebabCaseTestTest" + ); + }, +}); + +Deno.test({ + name: "camelCaseToKebabCase", + fn() { + assertEquals(camelCaseToKebabCase("camelCase"), "camel-case"); + assertEquals(camelCaseToKebabCase("camelCaseTest"), "camel-case-test"); + assertEquals( + camelCaseToKebabCase("camelCaseTestTest"), + "camel-case-test-test" + ); + assertEquals( + camelCaseToKebabCase("_camelCaseTestTest"), + "-camel-case-test-test" + ); + assertEquals( + camelCaseToKebabCase("camelCaseTestTest_"), + "camel-case-test-test-" + ); + assertEquals( + camelCaseToKebabCase("_camelCaseTestTest_"), + "-camel-case-test-test-" + ); + assertEquals( + camelCaseToKebabCase("camel_case_test_test"), + "camel-case-test-test" + ); + }, +}); + +Deno.test({ + name: "kebabCaseToCamelCase", + fn() { + assertEquals(kebabCaseToCamelCase("kebab-case"), "kebabCase"); + assertEquals(kebabCaseToCamelCase("kebab-Case-test"), "kebabCaseTest"); + assertEquals( + kebabCaseToCamelCase("kebab-case-test-test"), + "kebabCaseTestTest" + ); + assertEquals( + kebabCaseToCamelCase("-kebab-case-test-test"), + "kebabCaseTestTest" + ); + assertEquals( + kebabCaseToCamelCase("kebab-case-test-test-"), + "kebabCaseTestTest" + ); + assertEquals( + kebabCaseToCamelCase("-kebab-case-test-test-"), + "kebabCaseTestTest" + ); + assertEquals( + kebabCaseToCamelCase("kebabCaseTestTest"), + "kebabCaseTestTest" + ); + }, +}); + +Deno.test({ + name: "snakeCaseToPascalCase", + fn() { + assertEquals(snakeCaseToPascalCase("snake_case"), "SnakeCase"); + assertEquals(snakeCaseToPascalCase("snake_Case_test"), "SnakeCaseTest"); + assertEquals( + snakeCaseToPascalCase("snake_case_test_test"), + "SnakeCaseTestTest" + ); + assertEquals( + snakeCaseToPascalCase("_snake_case_test_test"), + "SnakeCaseTestTest" + ); + assertEquals( + snakeCaseToPascalCase("snake_case_test_test_"), + "SnakeCaseTestTest" + ); + assertEquals( + snakeCaseToPascalCase("_snake_case_test_test_"), + "SnakeCaseTestTest" + ); + assertEquals( + snakeCaseToPascalCase("snakeCaseTestTest"), + "SnakeCaseTestTest" + ); + }, +}); + +Deno.test({ + name: "pascalCaseToSnakeCase", + fn() { + assertEquals(pascalCaseToSnakeCase("PascalCase"), "pascal_case"); + assertEquals(pascalCaseToSnakeCase("PascalCaseTest"), "pascal_case_test"); + assertEquals( + pascalCaseToSnakeCase("PascalCaseTestTest"), + "pascal_case_test_test" + ); + assertEquals( + pascalCaseToSnakeCase("PascalCaseTestTest"), + "pascal_case_test_test" + ); + assertEquals( + pascalCaseToSnakeCase("PascalCaseTestTest"), + "pascal_case_test_test" + ); + assertEquals( + pascalCaseToSnakeCase("PascalCaseTestTest"), + "pascal_case_test_test" + ); + assertEquals( + pascalCaseToSnakeCase("pascal_case_test_test"), + "pascal_case_test_test" + ); + }, +}); + +Deno.test({ + name: "kebabCaseToPascalCase", + fn() { + assertEquals(kebabCaseToPascalCase("kebab-case"), "KebabCase"); + assertEquals(kebabCaseToPascalCase("kebab-Case-test"), "KebabCaseTest"); + assertEquals( + kebabCaseToPascalCase("kebab-case-test-test"), + "KebabCaseTestTest" + ); + assertEquals( + kebabCaseToPascalCase("-kebab-case-test-test"), + "KebabCaseTestTest" + ); + assertEquals( + kebabCaseToPascalCase("kebab-case-test-test-"), + "KebabCaseTestTest" + ); + assertEquals( + kebabCaseToPascalCase("-kebab-case-test-test-"), + "KebabCaseTestTest" + ); + assertEquals( + kebabCaseToPascalCase("kebabCaseTestTest"), + "KebabCaseTestTest" + ); + }, +}); + +Deno.test({ + name: "pascalCaseToKebabCase", + fn() { + assertEquals(pascalCaseToKebabCase("PascalCase"), "pascal-case"); + assertEquals(pascalCaseToKebabCase("PascalCaseTest"), "pascal-case-test"); + assertEquals( + pascalCaseToKebabCase("PascalCaseTestTest"), + "pascal-case-test-test" + ); + assertEquals( + pascalCaseToKebabCase("PascalCaseTestTest"), + "pascal-case-test-test" + ); + assertEquals( + pascalCaseToKebabCase("PascalCaseTestTest"), + "pascal-case-test-test" + ); + assertEquals( + pascalCaseToKebabCase("PascalCaseTestTest"), + "pascal-case-test-test" + ); + assertEquals( + pascalCaseToKebabCase("pascal-case-test-test"), + "pascal-case-test-test" + ); + }, +}); + +Deno.test({ + name: "camelCaseToPascalCase", + fn() { + assertEquals(camelCaseToPascalCase("camelCase"), "CamelCase"); + assertEquals(camelCaseToPascalCase("camelCaseTest"), "CamelCaseTest"); + assertEquals( + camelCaseToPascalCase("camelCaseTestTest"), + "CamelCaseTestTest" + ); + assertEquals( + camelCaseToPascalCase("camelCaseTestTest"), + "CamelCaseTestTest" + ); + assertEquals( + camelCaseToPascalCase("camelCaseTestTest"), + "CamelCaseTestTest" + ); + assertEquals( + camelCaseToPascalCase("camelCaseTestTest"), + "CamelCaseTestTest" + ); + }, +}); + +Deno.test({ + name: "pascalCaseToCamelCase", + fn() { + assertEquals(pascalCaseToCamelCase("PascalCase"), "pascalCase"); + assertEquals(pascalCaseToCamelCase("PascalCaseTest"), "pascalCaseTest"); + assertEquals( + pascalCaseToCamelCase("PascalCaseTestTest"), + "pascalCaseTestTest" + ); + assertEquals( + pascalCaseToCamelCase("PascalCaseTestTest"), + "pascalCaseTestTest" + ); + assertEquals( + pascalCaseToCamelCase("PascalCaseTestTest"), + "pascalCaseTestTest" + ); + assertEquals( + pascalCaseToCamelCase("PascalCaseTestTest"), + "pascalCaseTestTest" + ); + }, +}); + +Deno.test({ + name: "toTitleCase", + fn() { + assertEquals(toTitleCase("hello world"), "Hello World"); + assertEquals(toTitleCase("hello_world"), "Hello World"); + assertEquals(toTitleCase("hello-world"), "Hello World"); + assertEquals(toTitleCase("helloWorld"), "Hello World"); + assertEquals(toTitleCase("helloWorld"), "Hello World"); + }, +}); + +Deno.test({ + name: "toConstantCase", + fn() { + assertEquals(toConstantCase("hello world"), "HELLO_WORLD"); + assertEquals(toConstantCase("hello_world"), "HELLO_WORLD"); + assertEquals(toConstantCase("hello-world"), "HELLO_WORLD"); + assertEquals(toConstantCase("helloWorld"), "HELLO_WORLD"); + assertEquals(toConstantCase("helloWorld"), "HELLO_WORLD"); + }, +}); diff --git a/time/index.ts b/time/index.ts new file mode 100644 index 0000000..fdb8afb --- /dev/null +++ b/time/index.ts @@ -0,0 +1,176 @@ +import GoogleTypeDate from "npm:@alis-build/google-common-protos@latest/google/type/date_pb.js"; +import Timestamp from "npm:@alis-build/google-common-protos@latest/google/protobuf/timestamp_pb.js"; + +/** + * Parse a GoogleTypeDate or Timestamp instance into a Date instance + * + * @param time {GoogleTypeDate | Timestamp} - The time/date to parse + * @returns {Date | null | undefined} The parsed Date instance, or null if the input is nullish + */ +export const parse = ( + obj?: GoogleTypeDate.Date | Timestamp.Timestamp +): Date | null => { + // If time is nullish, return null + if (!obj) { + return null; + } + + // If time is an instance of GoogleTypeDate, return a new Date instance + if (obj instanceof GoogleTypeDate.Date) { + return new Date(obj.getYear(), obj.getMonth() - 1, obj.getDay()); + } + + // If time is an instance of Timestamp, return a new Date instance + if (obj instanceof Timestamp.Timestamp) { + const secondsInMillis = obj.getSeconds() * 1000; + const nanosInMillis = obj.getNanos() / 1000000; + return new Date(secondsInMillis + nanosInMillis); + } + + // If time is not an instance of GoogleTypeDate or Timestamp, return undefined + return null; +}; + +/** + * Encode a Date instance into a google.type.Date instance + * @param date {Date} - The date to encode + * @returns {GoogleTypeDate | null} The encoded Date instance, or null if the input is nullish + */ +export const encodeDate = (date: Date): GoogleTypeDate.Date | null => { + if (!date) { + return null; + } + + const googleDate = new GoogleTypeDate.Date(); + googleDate.setYear(date.getFullYear()); + googleDate.setMonth(date.getMonth() + 1); + googleDate.setDay(date.getDate()); + + return googleDate; +}; + +/** + * Encode a Date instance into a google.protobuf.Timestamp instance + * @param date {Date} - The date to encode + * @returns {Timestamp | null} The encoded Timestamp instance, or null if the input is nullish + */ +export const encodeTimestamp = (date: Date): Timestamp.Timestamp | null => { + if (!date) { + return null; + } + + const timestamp = new Timestamp.Timestamp(); + timestamp.setSeconds(Math.floor(date.getTime() / 1000)); + timestamp.setNanos((date.getTime() % 1000) * 1000000); + + return timestamp; +}; + +/** + * Format a distance between two dates in a human-readable format + * @param start {GoogleTypeDate | Timestamp | Date} - The start date + * @param end {GoogleTypeDate | Timestamp | Date} - Optional end date. If not provided, the current date is used + * @param relative {boolean} - Whether to return a relative time string (e.g. "5 minutes ago") or an absolute time string (e.g. "5 minutes") + * @returns {string} The formatted distance between the two dates + */ +export const formatDistance = ( + start: GoogleTypeDate.Date | Timestamp.Timestamp | Date, + end?: GoogleTypeDate.Date | Timestamp.Timestamp | Date, + relative = false +): string => { + if (!start) { + return ""; + } + + // Initialize null date + let startDate: Date | null = null; + let endDate: Date | null = null; + + // If startDate is an instance of GoogleTypeDate, return a new Date instance + if (start instanceof GoogleTypeDate.Date) { + startDate = new Date(start.getYear(), start.getMonth() - 1, start.getDay()); + } + + // If startDate is an instance of Timestamp, return a new Date instance + if (start instanceof Timestamp.Timestamp) { + const secondsInMillis = start.getSeconds() * 1000; + const nanosInMillis = start.getNanos() / 1000000; + startDate = new Date(secondsInMillis + nanosInMillis); + } + + // If startDate is an instance of Date, return a new Date instance + if (start instanceof Date) { + startDate = start; + } + + if (end) { + // If endDate is an instance of GoogleTypeDate, return a new Date instance + if (end instanceof GoogleTypeDate.Date) { + endDate = new Date(end.getYear(), end.getMonth() - 1, end.getDay()); + } + + // If endDate is an instance of Timestamp, return a new Date instance + if (end instanceof Timestamp.Timestamp) { + const secondsInMillis = end.getSeconds() * 1000; + const nanosInMillis = end.getNanos() / 1000000; + endDate = new Date(secondsInMillis + nanosInMillis); + } + + // If endDate is an instance of Date, return a new Date instance + if (end instanceof Date) { + endDate = end; + } + } else { + endDate = new Date(); + } + + // If start or end date is null, return an empty string + if (!startDate || !endDate) { + return ""; + } + + // Calculate the difference in seconds between the two dates + const diffInSeconds = Math.floor( + (endDate.getTime() - startDate.getTime()) / 1000 + ); + const isFuture = diffInSeconds < 0; + const diff = Math.abs(diffInSeconds); + + const units = [ + { name: "year", seconds: 31536000 }, + { name: "month", seconds: 2592000 }, + { name: "week", seconds: 604800 }, + { name: "day", seconds: 86400 }, + { name: "hour", seconds: 3600 }, + { name: "minute", seconds: 60 }, + ]; + + if (diff < 5) { + if (relative) { + return "just now"; + } + + const plural = diff > 1 || diff == 0 ? "s" : ""; + return `${diff} second${plural}`; + } + + for (const unit of units) { + const unitValue = Math.floor(diff / unit.seconds); + if (unitValue >= 1) { + const plural = unitValue > 1 ? "s" : ""; + const timeString = `${unitValue} ${unit.name}${plural}`; + + if (relative) { + return isFuture ? `in ${timeString}` : `${timeString} ago`; + } + + return timeString; + } + } + + if (relative) { + return isFuture ? "in a few seconds" : "a few seconds ago"; + } + + return "a few seconds"; +}; diff --git a/time/index_test.ts b/time/index_test.ts new file mode 100644 index 0000000..6c0d5da --- /dev/null +++ b/time/index_test.ts @@ -0,0 +1,174 @@ +import { assertEquals } from "https://deno.land/std@0.192.0/testing/asserts.ts"; +import { parse, formatDistance, encodeDate, encodeTimestamp } from "./index.ts"; +import GoogleTypeDate from "npm:@alis-build/google-common-protos@latest/google/type/date_pb.js"; +import Timestamp from "npm:@alis-build/google-common-protos@latest/google/protobuf/timestamp_pb.js"; + +Deno.test({ + name: "parseGoogleTypeDate", + fn(t) { + const date = new GoogleTypeDate.Date(); + date.setYear(2024); + date.setMonth(10); + date.setDay(1); + + const parsed = parse(date); + + assertEquals( + parsed?.getFullYear(), + 2024, + `Expected year to be 2024, but got ${parsed?.getFullYear()}` + ); + assertEquals( + parsed?.getMonth(), + 10 - 1, + `Expected month to be 10, but got ${parsed?.getMonth()}` + ); + assertEquals( + parsed?.getDate(), + 1, + `Expected date to be 1, but got ${parsed?.getDate()}` + ); + }, +}); + +Deno.test({ + name: "parseTimestamp", + fn(t) { + const time = new Date("2024-01-01T04:20:50.000Z"); + const timestamp = new Timestamp.Timestamp(); + timestamp.setSeconds(Math.floor(time.getTime() / 1000)); + timestamp.setNanos((time.getTime() % 1000) * 1000000); + + const parsed = parse(timestamp); + + assertEquals( + parsed?.getUTCFullYear(), + time.getUTCFullYear(), + `Expected year to be ${time.getUTCFullYear()}, but got ${parsed?.getUTCFullYear()}` + ); + assertEquals( + parsed?.getUTCMonth(), + time.getUTCMonth(), + `Expected month to be ${time.getUTCMonth()}, but got ${parsed?.getUTCMonth()}` + ); + assertEquals( + parsed?.getUTCDate(), + time.getUTCDate(), + `Expected date to be ${time.getUTCDate()}, but got ${parsed?.getUTCDate()}` + ); + assertEquals( + parsed?.getUTCHours(), + time.getUTCHours(), + `Expected hours to be ${time.getUTCHours()}, but got ${parsed?.getUTCHours()}` + ); + assertEquals( + parsed?.getUTCMinutes(), + time.getUTCMinutes(), + `Expected minutes to be ${time.getUTCMinutes()}, but got ${parsed?.getUTCMinutes()}` + ); + assertEquals( + parsed?.getUTCSeconds(), + time.getUTCSeconds(), + `Expected seconds to be ${time.getUTCSeconds()}, but got ${parsed?.getUTCSeconds()}` + ); + assertEquals( + parsed?.getUTCMilliseconds(), + time.getUTCMilliseconds(), + `Expected milliseconds to be ${time.getUTCMilliseconds()}, but got ${parsed?.getUTCMilliseconds()}` + ); + }, +}); + +Deno.test({ + name: "parseNullish", + fn(t) { + assertEquals(parse(), null); + assertEquals(parse(undefined), null); + }, +}); + +Deno.test({ + name: "formatDistance", + fn(t) { + const endTime = new Date("2024-10-15T04:20:50.000Z"); + const timeStrings = { + "2024-10-15T04:20:50.000Z": ["just now", "0 seconds"], + "2024-10-15T04:20:49.000Z": ["just now", "1 second"], + "2000-01-01T04:19:50.000Z": ["24 years ago", "24 years"], + "2028-10-15T04:20:50.000Z": ["in 4 years", "4 years"], + }; + + for (const [timeStr, expectedValues] of Object.entries(timeStrings)) { + const startTime = new Date(timeStr); + const timestamp = new Timestamp.Timestamp(); + timestamp.setSeconds(Math.floor(startTime.getTime() / 1000)); + timestamp.setNanos((startTime.getTime() % 1000) * 1000000); + + const formatted = formatDistance(timestamp, endTime, false); + assertEquals( + formatted, + expectedValues[1], + `Expected ${expectedValues[1]}, but got ${formatted}` + ); + + const formattedRelative = formatDistance(timestamp, endTime, true); + assertEquals( + formattedRelative, + expectedValues[0], + `Expected ${expectedValues[0]}, but got ${formattedRelative}` + ); + } + }, +}); + +Deno.test({ + name: "encodeDate", + fn(t) { + const date = new Date("2024-10-15T04:20:50.000Z"); + const googleDate = new GoogleTypeDate.Date(); + googleDate.setYear(date.getFullYear()); + googleDate.setMonth(date.getMonth() + 1); + googleDate.setDay(date.getDate()); + + const encoded = encodeDate(date); + + assertEquals( + encoded?.getYear(), + googleDate.getYear(), + `Expected year to be ${googleDate.getYear()}, but got ${encoded?.getYear()}` + ); + assertEquals( + encoded?.getMonth(), + googleDate.getMonth(), + `Expected month to be ${googleDate.getMonth()}, but got ${encoded?.getMonth()}` + ); + assertEquals( + encoded?.getDay(), + googleDate.getDay(), + `Expected date to be ${googleDate.getDay()}, but got ${encoded?.getDay()}` + ); + }, +}); + +Deno.test({ + name: "encodeTimestamp", + fn(t) { + const date = new Date("2024-10-15T04:20:50.000Z"); + const timestamp = new Timestamp.Timestamp(); + timestamp.setSeconds(Math.floor(date.getTime() / 1000)); + timestamp.setNanos((date.getTime() % 1000) * 1000000); + + const encoded = encodeTimestamp(date); + + assertEquals( + encoded?.getSeconds(), + timestamp.getSeconds(), + `Expected seconds to be ${timestamp.getSeconds()}, but got ${encoded?.getSeconds()}` + ); + assertEquals( + encoded?.getNanos(), + timestamp.getNanos(), + `Expected nanos to be ${timestamp.getNanos()}, but got ${encoded?.getNanos()}` + ); + }, +});