diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eed987..ec52fa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # @warkypublic/artemis-kit +## 1.0.10 + +### Patch Changes + +- 4058dd3: Added object retrocycle and decycle + +## 1.0.9 + +### Patch Changes + +- 0439207: Added tryFromBaseN and tryToBaseN + +## 1.0.8 + +### Patch Changes + +- 67a17e5: Added toBaseN and fromBaseN with test cases + +## 1.0.7 + +### Patch Changes + +- 1bd493a: Fixed object.getNestedValue to handle null values + ## 1.0.6 ### Patch Changes diff --git a/package.json b/package.json index e7cdcf0..eb979d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@warkypublic/artemis-kit", - "version": "1.0.6", + "version": "1.0.10", "description": "A comprehensive TypeScript/JavaScript utility library focused on precision and efficiency.", "type": "module", "main": "./src/index.ts", @@ -156,6 +156,7 @@ "typescript-eslint": "^8.17.0", "vite": "^6.0.2", "vite-plugin-dts": "^4.3.0", + "@anthropic-ai/claude-code": "^0.2.99", "vitest": "^2.1.8" }, "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 360e84b..f2cce1a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,9 @@ importers: specifier: ^11.0.3 version: 11.0.3 devDependencies: + '@anthropic-ai/claude-code': + specifier: ^0.2.99 + version: 0.2.99 '@changesets/cli': specifier: ^2.27.10 version: 2.27.10 @@ -51,6 +54,11 @@ importers: packages: + '@anthropic-ai/claude-code@0.2.99': + resolution: {integrity: sha512-KG+pFtiqSRmrSzcBRtgiPSqu9zaGg7GIlppKLEyDBlTn9M9JJbe1SLY5W8eB2AXC/fro+ePmM9cHzHO+zhkr7g==} + engines: {node: '>=18.0.0'} + hasBin: true + '@babel/helper-string-parser@7.25.9': resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} @@ -463,6 +471,45 @@ packages: resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} engines: {node: '>=18.18'} + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} @@ -827,10 +874,22 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + better-sqlite3@11.9.1: + resolution: {integrity: sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -841,6 +900,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -864,6 +926,9 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -918,10 +983,18 @@ packages: decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -933,10 +1006,17 @@ packages: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -1015,6 +1095,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + expect-type@1.1.0: resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} engines: {node: '>=12.0.0'} @@ -1046,6 +1130,9 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1069,6 +1156,9 @@ packages: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -1085,6 +1175,9 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1146,6 +1239,9 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1162,6 +1258,12 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + is-core-module@2.15.1: resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} engines: {node: '>= 0.4'} @@ -1284,6 +1386,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + minimatch@3.0.8: resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} @@ -1294,6 +1400,12 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mlly@1.7.3: resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} @@ -1312,12 +1424,22 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + node-abi@3.75.0: + resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==} + engines: {node: '>=10'} + nwsapi@2.2.16: resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1414,6 +1536,11 @@ packages: resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} engines: {node: ^10 || ^12 || >=14} + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + hasBin: true + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -1423,6 +1550,9 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1430,10 +1560,18 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -1468,6 +1606,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -1500,6 +1641,12 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -1528,6 +1675,9 @@ packages: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -1536,6 +1686,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -1555,6 +1709,13 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tar-fs@2.1.2: + resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -1606,6 +1767,9 @@ packages: peerDependencies: typescript: '>=4.2.0' + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1640,6 +1804,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@11.0.3: resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==} hasBin: true @@ -1792,6 +1959,9 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -1820,6 +1990,15 @@ packages: snapshots: + '@anthropic-ai/claude-code@0.2.99': + dependencies: + better-sqlite3: 11.9.1 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + '@babel/helper-string-parser@7.25.9': {} '@babel/helper-validator-identifier@7.25.9': {} @@ -2172,6 +2351,33 @@ snapshots: '@humanwhocodes/retry@0.4.1': {} + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + '@jridgewell/sourcemap-codec@1.5.0': {} '@manypkg/find-root@1.1.0': @@ -2571,10 +2777,27 @@ snapshots: balanced-match@1.0.2: {} + base64-js@1.5.1: {} + better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 + better-sqlite3@11.9.1: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -2588,6 +2811,11 @@ snapshots: dependencies: fill-range: 7.1.1 + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + cac@6.7.14: {} callsites@3.1.0: {} @@ -2609,6 +2837,8 @@ snapshots: check-error@2.1.1: {} + chownr@1.1.4: {} + ci-info@3.9.0: {} color-convert@2.0.1: @@ -2652,18 +2882,30 @@ snapshots: decimal.js@10.4.3: {} + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + deep-eql@5.0.2: {} + deep-extend@0.6.0: {} + deep-is@0.1.4: {} delayed-stream@1.0.0: {} detect-indent@6.1.0: {} + detect-libc@2.0.4: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -2802,6 +3044,8 @@ snapshots: esutils@2.0.3: {} + expand-template@2.0.3: {} + expect-type@1.1.0: {} extendable-error@0.1.7: {} @@ -2834,6 +3078,8 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-uri-to-path@1.0.0: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -2861,6 +3107,8 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + fs-constants@1.0.0: {} + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -2878,6 +3126,8 @@ snapshots: function-bind@1.1.2: {} + github-from-package@0.0.0: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -2939,6 +3189,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ieee754@1.2.1: {} + ignore@5.3.2: {} import-fresh@3.3.0: @@ -2950,6 +3202,10 @@ snapshots: imurmurhash@0.1.4: {} + inherits@2.0.4: {} + + ini@1.3.8: {} + is-core-module@2.15.1: dependencies: hasown: 2.0.2 @@ -3076,6 +3332,8 @@ snapshots: dependencies: mime-db: 1.52.0 + mimic-response@3.1.0: {} + minimatch@3.0.8: dependencies: brace-expansion: 1.1.11 @@ -3088,6 +3346,10 @@ snapshots: dependencies: brace-expansion: 2.0.1 + minimist@1.2.8: {} + + mkdirp-classic@0.5.3: {} + mlly@1.7.3: dependencies: acorn: 8.14.0 @@ -3103,10 +3365,20 @@ snapshots: nanoid@3.3.8: {} + napi-build-utils@2.0.0: {} + natural-compare@1.4.0: {} + node-abi@3.75.0: + dependencies: + semver: 7.6.3 + nwsapi@2.2.16: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3188,14 +3460,41 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.0.4 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.75.0 + pump: 3.0.2 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.2 + tunnel-agent: 0.6.0 + prelude-ls@1.2.1: {} prettier@2.8.8: {} + pump@3.0.2: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + punycode@2.3.1: {} queue-microtask@1.2.3: {} + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -3203,6 +3502,12 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + regenerator-runtime@0.14.1: {} require-from-string@2.0.2: {} @@ -3249,6 +3554,8 @@ snapshots: dependencies: queue-microtask: 1.2.3 + safe-buffer@5.2.1: {} + safer-buffer@2.1.2: {} saxes@6.0.0: @@ -3271,6 +3578,14 @@ snapshots: signal-exit@4.1.0: {} + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + slash@3.0.0: {} source-map-js@1.2.1: {} @@ -3290,12 +3605,18 @@ snapshots: string-argv@0.3.2: {} + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 strip-bom@3.0.0: {} + strip-json-comments@2.0.1: {} + strip-json-comments@3.1.1: {} supports-color@7.2.0: @@ -3310,6 +3631,21 @@ snapshots: symbol-tree@3.2.4: {} + tar-fs@2.1.2: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.2 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + term-size@2.2.1: {} tinybench@2.9.0: {} @@ -3348,6 +3684,10 @@ snapshots: dependencies: typescript: 5.7.2 + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -3375,6 +3715,8 @@ snapshots: dependencies: punycode: 2.3.1 + util-deprecate@1.0.2: {} + uuid@11.0.3: {} vite-node@2.1.8: @@ -3495,6 +3837,8 @@ snapshots: word-wrap@1.2.5: {} + wrappy@1.0.2: {} + ws@8.18.0: {} xml-name-validator@5.0.0: {} diff --git a/src/llm/claude.ts b/src/llm/claude.ts new file mode 100644 index 0000000..e13779b --- /dev/null +++ b/src/llm/claude.ts @@ -0,0 +1,108 @@ +interface ClaudeCompletionResponse { + id: string; + type: string; + role: string; + content: Array<{ + type: string; + text?: string; + }>; + model: string; + stop_reason: string; + stop_sequence: string | null; + usage: { + input_tokens: number; + output_tokens: number; + }; +} + +interface ClaudeCompletionOptions { + prompt: string; + options?: { + url?: string; + apiKey?: string; + model?: string; + maxTokens?: number; + temperature?: number; + topP?: number; + stopSequences?: string[]; + system?: string; + }; +} + +export async function getClaudeCompletion({ + prompt, + options: { + url = "https://api.anthropic.com/v1/messages", + apiKey = "x-demo", + model = "claude-3-sonnet-20240229", + maxTokens = 1024, + temperature = 0.7, + topP = 1, + stopSequences = [], + system = "", + } = {}, +}: ClaudeCompletionOptions): Promise { + try { + if (!prompt) { + throw new Error("Blank prompt"); + } + + if (!url) { + throw new Error("Claude API URL is not set"); + } + + if (!apiKey) { + throw new Error("Claude API key is not set"); + } + + const requestBody: any = { + model, + messages: [ + { + role: "user", + content: prompt, + }, + ], + max_tokens: maxTokens, + temperature, + top_p: topP, + }; + + if (stopSequences && stopSequences.length > 0) { + requestBody.stop_sequences = stopSequences; + } + + if (system) { + requestBody.system = system; + } + + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": apiKey, + "anthropic-version": "2023-06-01", + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + throw new Error(`Claude API error: ${response.statusText}`); + } + + const data: ClaudeCompletionResponse = await response.json(); + + // Extract text from the response + let responseText = ""; + for (const content of data.content) { + if (content.type === "text" && content.text) { + responseText += content.text; + } + } + + return responseText.trim(); + } catch (error) { + console.error("Error:", error); + throw error; + } +} \ No newline at end of file diff --git a/src/llm/index.test.ts b/src/llm/index.test.ts index e69de29..e99b9b5 100644 --- a/src/llm/index.test.ts +++ b/src/llm/index.test.ts @@ -0,0 +1,18 @@ +import { describe, it, expect } from "vitest"; +import { OpenAPI, Claude } from "./index"; + +describe("LLM Module", () => { + describe("OpenAPI", () => { + it("should export getTextCompletion", () => { + expect(OpenAPI.getTextCompletion).toBeDefined(); + expect(typeof OpenAPI.getTextCompletion).toBe("function"); + }); + }); + + describe("Claude", () => { + it("should export getClaudeCompletion", () => { + expect(Claude.getClaudeCompletion).toBeDefined(); + expect(typeof Claude.getClaudeCompletion).toBe("function"); + }); + }); +}); \ No newline at end of file diff --git a/src/llm/index.ts b/src/llm/index.ts index ce3f6dc..5e4e348 100644 --- a/src/llm/index.ts +++ b/src/llm/index.ts @@ -1,9 +1,15 @@ import { getTextCompletion } from "./openapi"; +import { getClaudeCompletion } from "./claude"; export const OpenAPI = { getTextCompletion, }; +export const Claude = { + getClaudeCompletion, +}; + export default { OpenAPI, + Claude, }; diff --git a/src/object/decycle.test.ts b/src/object/decycle.test.ts new file mode 100644 index 0000000..4a3294a --- /dev/null +++ b/src/object/decycle.test.ts @@ -0,0 +1,187 @@ +import { describe, it, expect } from 'vitest'; +import { decycle, retrocycle } from './decycle'; + +describe('decycle and retrocycle functions', () => { + it('should handle non-circular objects correctly', () => { + const obj = { a: 1, b: 'string', c: true }; + const decycled = decycle(obj); + expect(decycled).toEqual(obj); + + const restored = retrocycle(decycled); + expect(restored).toEqual(obj); + }); + + it('should handle arrays correctly', () => { + const arr = [1, 'string', true, { nested: 'object' }]; + const decycled = decycle(arr); + expect(decycled).toEqual(arr); + + const restored = retrocycle(decycled); + expect(restored).toEqual(arr); + }); + + it('should handle circular references in arrays', () => { + const arr: any[] = [1, 2, 3]; + arr.push(arr); // Create circular reference + + const decycled = decycle(arr); + expect(decycled).toEqual([1, 2, 3, { $ref: '$' }]); + + const restored = retrocycle(decycled); + expect(restored[0]).toBe(1); + expect(restored[1]).toBe(2); + expect(restored[2]).toBe(3); + expect(restored[3]).toBe(restored); // Circular reference restored + }); + + it('should handle circular references in objects', () => { + const obj: any = { a: 1, b: 'string' }; + obj.self = obj; // Create circular reference + + const decycled = decycle(obj); + expect(decycled).toEqual({ a: 1, b: 'string', self: { $ref: '$' } }); + + const restored = retrocycle(decycled); + expect(restored.a).toBe(1); + expect(restored.b).toBe('string'); + expect(restored.self).toBe(restored); // Circular reference restored + }); + + it('should handle nested objects with circular references', () => { + const inner: any = { x: 1 }; + const outer: any = { a: inner, b: 'string' }; + inner.parent = outer; // Create circular reference + + const decycled = decycle(outer); + expect(decycled).toEqual({ + a: { x: 1, parent: { $ref: '$' } }, + b: 'string' + }); + + const restored = retrocycle(decycled); + expect(restored.a.x).toBe(1); + expect(restored.a.parent).toBe(restored); // Circular reference restored + }); + + it('should handle multiple references to the same object', () => { + const shared = { id: 'shared' }; + const obj = { a: shared, b: shared }; + + const decycled = decycle(obj); + expect(decycled).toEqual({ + a: { id: 'shared' }, + b: { $ref: '$["a"]' } + }); + + const restored = retrocycle(decycled); + expect(restored.a).toBe(restored.b); // Both refer to the same object + }); + + it('should work with custom replacer function', () => { + const obj = { a: 1, b: 2, secret: 'hidden' }; + const replacer = (value: any) => { + if (value && typeof value === 'object' && 'secret' in value) { + const copy = { ...value }; + copy.secret = '[REDACTED]'; + return copy; + } + return value; + }; + + const decycled = decycle(obj, replacer); + expect(decycled).toEqual({ a: 1, b: 2, secret: '[REDACTED]' }); + }); + + it('should handle null and undefined values', () => { + const obj = { a: null, b: undefined, c: { d: null } }; + const decycled = decycle(obj); + expect(decycled).toEqual(obj); + + const restored = retrocycle(decycled); + expect(restored).toEqual(obj); + }); + + it('should handle built-in objects like Date, RegExp, and more', () => { + const date = new Date('2023-01-01'); + const regexp = /test/g; + const obj = { + date, + regexp, + bool: new Boolean(true), + num: new Number(42), + str: new String("test") + }; + + const decycled = decycle(obj); + expect(decycled.date).toBeInstanceOf(Date); + expect(decycled.regexp).toBeInstanceOf(RegExp); + expect(decycled.bool).toBeInstanceOf(Boolean); + expect(decycled.num).toBeInstanceOf(Number); + expect(decycled.str).toBeInstanceOf(String); + + const restored = retrocycle(decycled); + expect(restored.date).toBeInstanceOf(Date); + expect(restored.date.toISOString()).toBe(date.toISOString()); + expect(restored.regexp).toBeInstanceOf(RegExp); + expect(restored.regexp.source).toBe(regexp.source); + }); + + it('should handle complex nested circular references', () => { + const a: any = { name: 'a' }; + const b: any = { name: 'b' }; + const c: any = { name: 'c' }; + + a.child = b; + b.child = c; + c.parent = a; // Circular reference + + const decycled = decycle(a); + expect(decycled).toEqual({ + name: 'a', + child: { + name: 'b', + child: { + name: 'c', + parent: { $ref: '$' } + } + } + }); + + const restored = retrocycle(decycled); + expect(restored.name).toBe('a'); + expect(restored.child.name).toBe('b'); + expect(restored.child.child.name).toBe('c'); + expect(restored.child.child.parent).toBe(restored); // Circular reference restored + }); + + it('should handle arrays with multiple circular references', () => { + const arr1: any[] = [1, 2]; + const arr2: any[] = [3, 4]; + arr1.push(arr2); + arr2.push(arr1); // Circular reference + + const decycled = decycle(arr1); + expect(decycled).toEqual([1, 2, [3, 4, { $ref: '$' }]]); + + const restored = retrocycle(decycled); + expect(restored[0]).toBe(1); + expect(restored[1]).toBe(2); + expect(restored[2][0]).toBe(3); + expect(restored[2][1]).toBe(4); + expect(restored[2][2]).toBe(restored); // Circular reference restored + }); + + it('should work with JSON.stringify and JSON.parse for circular structures', () => { + const obj: any = { a: 1, b: 'string' }; + obj.self = obj; // Create circular reference + + const decycled = decycle(obj); + const jsonStr = JSON.stringify(decycled); + const parsed = JSON.parse(jsonStr); + const restored = retrocycle(parsed); + + expect(restored.a).toBe(1); + expect(restored.b).toBe('string'); + expect(restored.self).toBe(restored); // Circular reference restored + }); +}); \ No newline at end of file diff --git a/src/object/decycle.ts b/src/object/decycle.ts new file mode 100644 index 0000000..b8357d4 --- /dev/null +++ b/src/object/decycle.ts @@ -0,0 +1,146 @@ +/** + * Interface for an object with a $ref property pointing to a path + */ +export interface RefObject { + $ref: string; +} + +/** + * Makes a deep copy of an object or array, replacing circular references + * with objects of the form {"$ref": PATH} where PATH is a JSONPath string + * that locates the first occurrence. + * + * @param object - The object to decycle + * @param replacer - Optional function to replace values during the process + * @returns A decycled copy of the object + */ +export function decycle( + object: T, + replacer?: (value: any) => any +): any { + "use strict"; + + // Object to path mappings + const objects = new WeakMap(); + + return (function derez(value: any, path: string): any { + // The path of an earlier occurrence of value + let old_path: string | undefined; + // The new object or array + let nu: any; + + // If a replacer function was provided, then call it to get a replacement value. + if (replacer !== undefined) { + value = replacer(value); + } + + // typeof null === "object", so go on if this value is really an object but not + // one of the weird builtin objects. + if ( + typeof value === "object" + && value !== null + && !(value instanceof Boolean) + && !(value instanceof Date) + && !(value instanceof Number) + && !(value instanceof RegExp) + && !(value instanceof String) + ) { + // If the value is an object or array, look to see if we have already + // encountered it. If so, return a {"$ref":PATH} object. This uses an + // ES6 WeakMap. + old_path = objects.get(value); + if (old_path !== undefined) { + return {$ref: old_path}; + } + + // Otherwise, accumulate the unique value and its path. + objects.set(value, path); + + // If it is an array, replicate the array. + if (Array.isArray(value)) { + nu = []; + value.forEach(function (element, i) { + nu[i] = derez(element, path + "[" + i + "]"); + }); + } else { + // If it is an object, replicate the object. + nu = {}; + Object.keys(value).forEach(function (name) { + nu[name] = derez( + value[name], + path + "[" + JSON.stringify(name) + "]" + ); + }); + } + return nu; + } + return value; + }(object, "$")); +} + +/** + * Restores an object that was reduced by decycle. Members whose values are + * objects of the form {$ref: PATH} are replaced with references to the value + * found by the PATH. This will restore cycles. + * + * @param $ - The object to restore cycles in + * @returns The object with restored cycles + */ +export function retrocycle($: T): T { + "use strict"; + + // Regular expression to validate JSONPath format + // NOTE: This function uses eval() which can be a security risk. + // Consider using a safer alternative in production environments. + // eslint-disable-next-line no-control-regex + const px = /^\$(?:\[(?:\d+|"(?:[^\\"\u0000-\u001f]|\\(?:[\\"\/bfnrt]|u[0-9a-zA-Z]{4}))*")\])*$/; + + (function rez(value: any): void { + // The rez function walks recursively through the object looking for $ref + // properties. When it finds one that has a value that is a path, then it + // replaces the $ref object with a reference to the value that is found by + // the path. + if (value && typeof value === "object") { + if (Array.isArray(value)) { + value.forEach(function (element, i) { + if (typeof element === "object" && element !== null) { + const path = element.$ref; + if (typeof path === "string" && px.test(path)) { + // Security warning: eval is used here + value[i] = eval(path); + } else { + rez(element); + } + } + }); + } else { + Object.keys(value).forEach(function (name) { + const item = value[name]; + if (typeof item === "object" && item !== null) { + const path = item.$ref; + if (typeof path === "string" && px.test(path)) { + // Security warning: eval is used here + value[name] = eval(path); + } else { + rez(item); + } + } + }); + } + } + }($)); + return $; +} + +/** + * + * @description Converts a object with circular references to JSON + * @param json + * @param object + * @returns + */ +export function stringify_json( + object: T, +) { + return JSON.stringify(retrocycle(object)) +} \ No newline at end of file diff --git a/src/object/index.ts b/src/object/index.ts index f9ac6c7..bb63fb9 100644 --- a/src/object/index.ts +++ b/src/object/index.ts @@ -1,3 +1,5 @@ export {getNestedValue,setNestedValue} from './nested' export {objectCompare} from './compare' -export {createSelectOptions} from './util' \ No newline at end of file +export {createSelectOptions} from './util' +export {decycle,retrocycle, stringify_json} from './decycle' +export type {RefObject} from './decycle' \ No newline at end of file diff --git a/src/object/nested.test.ts b/src/object/nested.test.ts index 907c8ac..cb56351 100644 --- a/src/object/nested.test.ts +++ b/src/object/nested.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { getNestedValue, setNestedValue } from "./nested"; +import { findObjectByKeyValues, findObjectPath, getNestedValue, setNestedValue } from "./nested"; describe("getNestedValue", () => { const testObj = { @@ -170,3 +170,178 @@ describe("setNestedValue", () => { expect(obj.list[0]).toBe("item"); }); }); + + + +describe("findObjectPath", () => { + const testObj = { + user: { + name: "John", + id: 1, + contacts: [ + { id: 101, email: "john@example.com", phone: "123-456" }, + { id: 102, email: "john.alt@example.com", phone: "789-012" }, + ], + settings: { + id: 201, + notifications: { + email: true, + push: false, + }, + }, + }, + meta: { + created: "2023-01-01", + tags: ["important", "user"], + }, + items: [ + { id: 1, name: "Item 1" }, + { id: 2, name: "Item 2" }, + { id: 1, name: "Item 3" }, // Duplicate ID + ], + config: { + defaultUser: { id: 1 } + } + }; + + test("should find simple property matches", () => { + const paths = findObjectPath(testObj, { id: 1 }); + expect(paths).toContain("user"); + expect(paths).toContain("items[0]"); + expect(paths).toContain("items[2]"); + expect(paths).toContain("config.defaultUser"); + }); + + test("should find nested property matches", () => { + const paths = findObjectPath(testObj, { email: true }); + expect(paths).toContain("user.settings.notifications"); + }); + + test("should find matches in arrays", () => { + const paths = findObjectPath(testObj, { id: 101 }); + expect(paths).toContain("user.contacts[0]"); + + const paths2 = findObjectPath(testObj, { id: 102 }); + expect(paths2).toContain("user.contacts[1]"); + }); + + test("should find matches with multiple criteria", () => { + const paths = findObjectPath(testObj, { id: 1, name: "Item 1" }); + expect(paths).toContain("items[0]"); + expect(paths).not.toContain("items[2]"); // Has id: 1 but different name + expect(paths).not.toContain("user"); // Has id: 1 but no name property + }); + + test("should return empty array for no matches", () => { + const paths = findObjectPath(testObj, { id: 999 }); + expect(paths).toEqual([]); + }); + + test("should handle empty criteria", () => { + const paths = findObjectPath(testObj, {}); + expect(paths).toEqual([]); + }); + + test("should handle null or undefined input", () => { + expect(findObjectPath(null, { id: 1 })).toEqual([]); + expect(findObjectPath(undefined, { id: 1 })).toEqual([]); + expect(findObjectPath(testObj, null)).toEqual([]); + expect(findObjectPath(testObj, undefined)).toEqual([]); + }); + + test("should work with setNestedValue", () => { + const obj = { ...testObj }; // Clone to avoid modifying test object + const paths = findObjectPath(obj, { id: 101 }); + expect(paths.length).toBeGreaterThan(0); + + const path = paths[0]; + const originalValue = getNestedValue(path, obj); + const updatedValue = { ...originalValue, email: "updated@example.com" }; + + setNestedValue(path, updatedValue, obj); + expect(getNestedValue(`${path}.email`, obj)).toBe("updated@example.com"); + }); + + test("should find path to primitive array values", () => { + const paths = findObjectPath(testObj, { 0: "important" }); + expect(paths).toContain("meta.tags"); + }); +}); + +// Only add these tests if you're implementing the advanced version +describe("findObjectByKeyValues", () => { + const testObj = { + user: { + name: "John", + id: 1, + contacts: [ + { id: 101, email: "john@example.com", phone: "123-456" }, + { id: 102, email: "JOHN.alt@example.com", phone: "789-012" }, + ], + }, + items: [ + { id: 1, name: "Item 1", status: "active" }, + { id: 2, name: "Item 2", status: "inactive" }, + { id: 3, name: "Item 3", status: "active" }, + ], + }; + + test("should handle partial matches", () => { + const paths = findObjectByKeyValues(testObj, { id: 1, name: "Unknown" }, { partialMatch: true }); + expect(paths).toContain("user"); + expect(paths).toContain("items[0]"); + }); + + test("should handle maxDepth option", () => { + // Should not find matches beyond depth 1 + const paths = findObjectByKeyValues(testObj, { id: 101 }, { maxDepth: 1 }); + expect(paths).toEqual([]); + + // Should not find matches at depth 2 + const paths2 = findObjectByKeyValues(testObj, { id: 101 }, { maxDepth: 3 }); + expect(paths2).toContain("user.contacts[0]"); + }); + + test("should handle returnFirstMatch option", () => { + // Should return only the first match + const paths = findObjectByKeyValues(testObj, { status: "active" }, { returnFirstMatch: true }); + expect(paths.length).toBe(1); + expect(paths[0]).toBe("items[0]"); + }); + + test("should handle case insensitive matching", () => { + // Should match regardless of case + const paths = findObjectByKeyValues(testObj, { email: "john.alt@example.com" }, { caseSensitive: false }); + expect(paths).toContain("user.contacts[1]"); + }); + + test("should use custom comparison function", () => { + // Custom comparator that checks if string contains substring + const customCompare = (a: any, b: any) => { + if (typeof a === 'string' && typeof b === 'string') { + return a.includes(b); + } + return a === b; + }; + + const paths = findObjectByKeyValues(testObj, { email: "@example" }, { customCompare }); + expect(paths).toContain("user.contacts[0]"); + expect(paths).toContain("user.contacts[1]"); + }); + + test("should combine multiple options", () => { + const paths = findObjectByKeyValues( + testObj, + { id: 1, status: "active" }, + { + partialMatch: true, + maxDepth: 2, + returnFirstMatch: true + } + ); + + expect(paths.length).toBe(1); + // Either user or items[0] could be returned since we're stopping at first match + expect(paths[0] === "user" || paths[0] === "items[0]").toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/src/object/nested.ts b/src/object/nested.ts index 08df1c6..95abc32 100644 --- a/src/object/nested.ts +++ b/src/object/nested.ts @@ -20,7 +20,8 @@ export function getNestedValue(path: string, obj: Record): any { if (parts.length === 0) return undefined; return parts.reduce((prev, curr) => { - if (prev === undefined) return undefined; + if (prev === null || prev === undefined) return undefined; + if (typeof prev !== "object") return undefined; return prev[curr]; }, obj); } @@ -101,3 +102,176 @@ export function setNestedValue( current[lastKey] = value; return obj; } +export function findObjectByKeyValues( + obj: Record, + criteria: Record, + options: { + partialMatch?: boolean; + maxDepth?: number; + returnFirstMatch?: boolean; + caseSensitive?: boolean; + customCompare?: (a: any, b: any) => boolean; + } = {} +): string[] { + // Handle invalid inputs + if (!obj || typeof obj !== 'object') { + return []; + } + + // Handle empty criteria + if (!criteria || typeof criteria !== 'object' || Object.keys(criteria).length === 0) { + return []; + } + + const { + partialMatch = false, + maxDepth = Infinity, + returnFirstMatch = false, + caseSensitive = true, + customCompare + } = options; + + const results: string[] = []; + + // Helper function for comparing values + const compareValues = (a: any, b: any): boolean => { + if (customCompare) { + return customCompare(a, b); + } + + // Handle string comparison with case sensitivity option + if (typeof a === 'string' && typeof b === 'string' && !caseSensitive) { + return a.toLowerCase() === b.toLowerCase(); + } + + return a === b; + }; + + // Recursive search function + const search = ( + currentObj: Record, + currentPath: string = '', + depth: number = 0 + ): void => { + // Check depth first - don't process objects beyond max depth + if (depth > maxDepth) { + return; + } + + // Skip if not a valid object + if (!currentObj || typeof currentObj !== 'object') { + return; + } + + // Check if current object matches criteria + let matchCount = 0; + const criteriaKeys = Object.keys(criteria); + + for (const key of criteriaKeys) { + if (key in currentObj && compareValues(currentObj[key], criteria[key])) { + matchCount++; + } + } + + const isMatch = partialMatch + ? matchCount > 0 + : matchCount === criteriaKeys.length; + + // If matched, add to results + if (isMatch && criteriaKeys.length > 0) { + results.push(currentPath); + if (returnFirstMatch) { + return; + } + } + + // Recursively search nested objects and arrays + if (Array.isArray(currentObj)) { + for (let i = 0; i < currentObj.length; i++) { + const item = currentObj[i]; + if (item && typeof item === 'object') { + const nextPath = currentPath ? `${currentPath}[${i}]` : `[${i}]`; + search(item, nextPath, depth + 1); + if (returnFirstMatch && results.length > 0) { + return; + } + } + } + } else { + for (const [key, value] of Object.entries(currentObj)) { + if (value && typeof value === 'object') { + const nextPath = currentPath ? `${currentPath}.${key}` : key; + search(value, nextPath, depth + 1); + if (returnFirstMatch && results.length > 0) { + return; + } + } + } + } + }; + + search(obj); + return results; +} + + + +/** + * Finds objects in a nested structure that match specified key-value pairs + * Returns paths compatible with setNestedValue function + * @param obj - Source object to search within + * @param criteria - Object with key-value pairs to match against + * @returns Array of paths where matching objects were found + */ +export function findObjectPath( + obj: Record, + criteria: Record +): string[] { + const results: string[] = []; + + // Handle invalid inputs + if (!obj || typeof obj !== 'object') { + return results; + } + + // Handle empty criteria - should return empty array + if (!criteria || typeof criteria !== 'object' || Object.keys(criteria).length === 0) { + return results; + } + + const search = (currentObj: any, path: string = '') => { + // Skip if not an object or is null + if (!currentObj || typeof currentObj !== 'object') { + return; + } + + // Check if current object matches all criteria + let matches = true; + for (const [key, value] of Object.entries(criteria)) { + if (!(key in currentObj) || currentObj[key] !== value) { + matches = false; + break; + } + } + + if (matches) { + results.push(path); + } + + // Continue searching in nested properties + if (Array.isArray(currentObj)) { + for (let i = 0; i < currentObj.length; i++) { + const newPath = path ? `${path}[${i}]` : `[${i}]`; + search(currentObj[i], newPath); + } + } else { + for (const [key, value] of Object.entries(currentObj)) { + const newPath = path ? `${path}.${key}` : key; + search(value, newPath); + } + } + }; + + search(obj); + return results; +} \ No newline at end of file diff --git a/src/strings/baseNumber.test.ts b/src/strings/baseNumber.test.ts new file mode 100644 index 0000000..0193a59 --- /dev/null +++ b/src/strings/baseNumber.test.ts @@ -0,0 +1,184 @@ +import { describe, it, expect } from 'vitest'; +import { fromBaseN, toBaseN } from './baseNumber'; // Update with actual path + +// This test suite targets ES6+ environments + +describe('Base-N Converters', () => { + describe('fromBaseN', () => { + // Basic conversion tests + it('should convert base 10 strings correctly', () => { + expect(fromBaseN('0', 10)).toBe(BigInt(0)); + expect(fromBaseN('123', 10)).toBe(BigInt(123)); + expect(fromBaseN('9999', 10)).toBe(BigInt(9999)); + }); + + it('should convert base 2 (binary) strings correctly', () => { + expect(fromBaseN('0', 2)).toBe(BigInt(0)); + expect(fromBaseN('1', 2)).toBe(BigInt(1)); + expect(fromBaseN('10', 2)).toBe(BigInt(2)); + expect(fromBaseN('1010', 2)).toBe(BigInt(10)); + expect(fromBaseN('11111111', 2)).toBe(BigInt(255)); + }); + + it('should convert base 16 (hex) strings correctly', () => { + expect(fromBaseN('0', 16)).toBe(BigInt(0)); + expect(fromBaseN('A', 16)).toBe(BigInt(10)); + expect(fromBaseN('F', 16)).toBe(BigInt(15)); + expect(fromBaseN('10', 16)).toBe(BigInt(16)); + expect(fromBaseN('FF', 16)).toBe(BigInt(255)); + expect(fromBaseN('ABC', 16)).toBe(BigInt(2748)); + expect(fromBaseN('DEADBEEF', 16)).toBe(BigInt(3735928559)); + }); + + it('should convert base 36 strings correctly', () => { + expect(fromBaseN('0', 36)).toBe(BigInt(0)); + expect(fromBaseN('Z', 36)).toBe(BigInt(35)); + expect(fromBaseN('10', 36)).toBe(BigInt(36)); + expect(fromBaseN('CLAUDE', 36)).toBe(BigInt(761371970)); + }); + + // Case insensitivity + it('should be case insensitive', () => { + expect(fromBaseN('abc', 16)).toBe(BigInt(2748)); + expect(fromBaseN('ABC', 16)).toBe(BigInt(2748)); + expect(fromBaseN('AbC', 16)).toBe(BigInt(2748)); + expect(fromBaseN('claude', 36)).toBe(BigInt(761371970)); + expect(fromBaseN('CLAUDE', 36)).toBe(BigInt(761371970)); + }); + + // Default base + it('should use base 36 by default', () => { + expect(fromBaseN('Z')).toBe(BigInt(35)); + expect(fromBaseN('10')).toBe(BigInt(36)); + }); + + // Edge cases + it('should handle empty string or null', () => { + expect(fromBaseN('')).toBe(BigInt(0)); + expect(fromBaseN('', 10)).toBe(BigInt(0)); + }); + + it('should handle very large numbers', () => { + expect(fromBaseN('ZZZZZZZZZZZZ', 36)).toBe(BigInt('4738381338321616895')); + }); + + // Error cases + it('should throw error for invalid base', () => { + expect(() => fromBaseN('123', 1)).toThrow('Base must be between 2 and 36'); + expect(() => fromBaseN('123', 37)).toThrow('Base must be between 2 and 36'); + }); + + it('should throw error for invalid characters', () => { + expect(() => fromBaseN('12$3', 10)).toThrow('Invalid character in input string: $'); + expect(() => fromBaseN('XYZ', 10)).toThrow('Digit Z is invalid for base 10'); + }); + + it('should throw error for digit out of range for base', () => { + expect(() => fromBaseN('129', 9)).toThrow('Digit 9 is invalid for base 9'); + expect(() => fromBaseN('12A', 10)).toThrow('Digit A is invalid for base 10'); + }); + }); + + describe('toBaseN', () => { + // Basic conversion tests + it('should convert to base 10 strings correctly', () => { + expect(toBaseN(0, 10)).toBe('0'); + expect(toBaseN(123, 10)).toBe('123'); + expect(toBaseN(9999, 10)).toBe('9999'); + }); + + it('should convert to base 2 (binary) strings correctly', () => { + expect(toBaseN(0, 2)).toBe('0'); + expect(toBaseN(1, 2)).toBe('1'); + expect(toBaseN(2, 2)).toBe('10'); + expect(toBaseN(10, 2)).toBe('1010'); + expect(toBaseN(255, 2)).toBe('11111111'); + }); + + it('should convert to base 16 (hex) strings correctly', () => { + expect(toBaseN(0, 16)).toBe('0'); + expect(toBaseN(10, 16)).toBe('A'); + expect(toBaseN(15, 16)).toBe('F'); + expect(toBaseN(16, 16)).toBe('10'); + expect(toBaseN(255, 16)).toBe('FF'); + expect(toBaseN(2748, 16)).toBe('ABC'); + expect(toBaseN(3735928559, 16)).toBe('DEADBEEF'); + }); + + it('should convert to base 36 strings correctly', () => { + expect(toBaseN(0, 36)).toBe('0'); + expect(toBaseN(35, 36)).toBe('Z'); + expect(toBaseN(36, 36)).toBe('10'); + expect(toBaseN(761371970, 36)).toBe('CLAUDE'); + }); + + // Default base + it('should use base 36 by default', () => { + expect(toBaseN(35)).toBe('Z'); + expect(toBaseN(36)).toBe('10'); + }); + + // Edge cases + it('should handle zero', () => { + expect(toBaseN(0)).toBe('0'); + expect(toBaseN(BigInt(0))).toBe('0'); + }); + + it('should handle negative numbers', () => { + expect(toBaseN(-123, 10)).toBe('-123'); + expect(toBaseN(-15, 16)).toBe('-F'); + expect(toBaseN(-35, 36)).toBe('-Z'); + }); + + it('should handle BigInt inputs', () => { + expect(toBaseN(BigInt(123), 10)).toBe('123'); + expect(toBaseN(BigInt('9007199254740991'), 16)).toBe('1FFFFFFFFFFFFF'); + expect(toBaseN(BigInt('4738381338321616895'), 36)).toBe('ZZZZZZZZZZZZ'); + }); + + // Error cases + it('should throw error for invalid base', () => { + expect(() => toBaseN(123, 1)).toThrow('Base must be between 2 and 36'); + expect(() => toBaseN(123, 37)).toThrow('Base must be between 2 and 36'); + }); + }); + + // Round-trip tests + describe('round-trip conversions', () => { + it('should preserve value when converting back and forth', () => { + const testValues = [ + 0, 1, 42, 123, 255, 1000, 9999, 65535, 16777215, 2147483647, + // Large values as BigInt + BigInt('4738381338321616895'), + BigInt('761371970') // Value for 'CLAUDE' in base 36 + ]; + + const bases = [2, 8, 10, 16, 36]; + + for (const value of testValues) { + for (const base of bases) { + // Convert to string in base-n, then back to number + const baseStr = toBaseN(value, base); + const backToNumber = fromBaseN(baseStr, base); + + // Convert BigInt to string for comparison since BigInt !== number + const originalValue = typeof value === 'bigint' ? value : BigInt(value); + expect(backToNumber).toBe(originalValue); + } + } + }); + + it('should handle random values in round-trip conversions', () => { + // Test with 10 random values + for (let i = 0; i < 10; i++) { + const randomValue = Math.floor(Math.random() * 1000000); + const randomBase = Math.floor(Math.random() * 35) + 2; // Random base between 2-36 + + const baseStr = toBaseN(randomValue, randomBase); + const backToNumber = fromBaseN(baseStr, randomBase); + + expect(backToNumber).toBe(BigInt(randomValue)); + } + }); + }); +}); \ No newline at end of file diff --git a/src/strings/baseNumber.ts b/src/strings/baseNumber.ts new file mode 100644 index 0000000..4cfff14 --- /dev/null +++ b/src/strings/baseNumber.ts @@ -0,0 +1,124 @@ +/** + * Convert a string in a given base to a decimal number + * This implementation targets ES6+ and uses BigInt for large number support + * + * @param str The string to convert + * @param base The base to convert from (2-36) + * @returns The decimal number as BigInt + */ +export function fromBaseN(str: string, base: number = 36): bigint { + // Input validation + if (base < 2 || base > 36) { + throw new Error('Base must be between 2 and 36'); + } + + // Convert empty string or null to 0 + if (!str) { + return BigInt(0); + } + + // Use standard alphanumeric characters for conversion: 0-9, A-Z + const lookupString = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + let result = BigInt(0); + let power = BigInt(1); + + // Process string right to left + for (let i = str.length - 1; i >= 0; i--) { + const char = str[i].toUpperCase(); + const digit = lookupString.indexOf(char); + + // Check if character is valid + if (digit === -1) { + throw new Error(`Invalid character in input string: ${char}`); + } + + // Validate digit is within base range + if (digit >= base) { + throw new Error(`Digit ${char} is invalid for base ${base}`); + } + + result += BigInt(digit) * power; + power *= BigInt(base); + } + + return result; +} + +/** + * Convert a decimal number to a string in a given base + * This implementation targets ES6+ and uses BigInt for large number support + * + * @param num The decimal number to convert + * @param base The base to convert to (2-36) + * @returns The string representation in the specified base + */ +export function toBaseN(num: number | bigint, base: number = 36): string { + // Input validation + if (base < 2 || base > 36) { + throw new Error('Base must be between 2 and 36'); + } + + // Handle 0 separately + if (num === 0 || num === BigInt(0)) { + return '0'; + } + + // Convert to BigInt to handle large numbers + const bigNum = typeof num === 'number' ? BigInt(num) : num; + + // Use standard alphanumeric characters for conversion: 0-9, A-Z + const lookupString = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + let result = ''; + let remaining = bigNum > BigInt(0) ? bigNum : -bigNum; // Handle negative numbers + const bigBase = BigInt(base); + + while (remaining > BigInt(0)) { + const digitValue = Number(remaining % bigBase); + const digitChar = lookupString[digitValue]; + + result = digitChar + result; + remaining = remaining / bigBase; + } + + // Add negative sign if necessary + return bigNum < BigInt(0) ? '-' + result : result; +} + + +/** + * Convert a string in a given base to a decimal number, if it fails, returns fallback + * This implementation targets ES6+ and uses BigInt for large number support + * + * @param str The string to convert + * @param base The base to convert from (2-36) + * @returns The decimal number as BigInt + */ +export function tryFromBaseN(str: string, base: number = 36, fallback: bigint | number = 0): bigint| number { + try { + return fromBaseN(str, base) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch(_e) { + + return fallback + } + +} + +/** + * Convert a decimal number to a string in a given base, it it fails, use fallback + * This implementation targets ES6+ and uses BigInt for large number support + * + * @param num The decimal number to convert + * @param base The base to convert to (2-36) + * @returns The string representation in the specified base + */ +export function tryToBaseN(num: number | bigint, base: number = 36, fallback: string = ""): string { + try { + return toBaseN(num, base) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch(_e) { + + return fallback + } + +} \ No newline at end of file diff --git a/src/strings/index.ts b/src/strings/index.ts index 8cd54ee..4536da6 100644 --- a/src/strings/index.ts +++ b/src/strings/index.ts @@ -7,3 +7,4 @@ export * from "./legacy"; export * from "./uuid"; export * from "./time"; export * from "./blankValue"; +export * from "./baseNumber";