├── .all-contributorsrc ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── bug_types.yml │ └── feature_request.yml ├── pull_request_template.md └── workflows │ └── workflow.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ava.config.js ├── benchmark ├── spyd.yml ├── string.js └── tasks.js ├── eslint.config.js ├── gulpfile.js ├── package-lock.json ├── package.json ├── prettier.config.js ├── src ├── buffer.js ├── char_code.js ├── char_code.test.js ├── encoder.js ├── encoder.test.js ├── helpers │ ├── main.test.js │ └── strings.test.js ├── main.d.ts ├── main.js ├── main.test-d.ts └── main.test.js └── tsconfig.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "string-byte-length", 3 | "projectOwner": "ehmicky", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "linkToUsage": false, 12 | "contributors": [ 13 | { 14 | "login": "ehmicky", 15 | "name": "ehmicky", 16 | "avatar_url": "https://avatars2.githubusercontent.com/u/8136211?v=4", 17 | "profile": "https://fosstodon.org/@ehmicky", 18 | "contributions": [ 19 | "code", 20 | "design", 21 | "ideas", 22 | "doc" 23 | ] 24 | } 25 | ], 26 | "contributorsPerLine": 7, 27 | "skipCi": true, 28 | "commitConvention": "none" 29 | } 30 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | max_line_length = 80 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report a bug 3 | title: Please replace with a clear and descriptive title 4 | labels: [bug] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Thanks for reporting this bug! 9 | - type: checkboxes 10 | attributes: 11 | label: Guidelines 12 | options: 13 | - label: 14 | Please search other issues to make sure this bug has not already 15 | been reported. 16 | required: true 17 | - label: 18 | If this is related to a typo or the documentation being unclear, 19 | please click on the relevant page's `Edit` button (pencil icon) and 20 | suggest a correction instead. 21 | required: true 22 | - type: textarea 23 | attributes: 24 | label: Describe the bug 25 | placeholder: A clear and concise description of what the bug is. 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: Steps to reproduce 31 | placeholder: | 32 | Step-by-step instructions on how to reproduce the behavior. 33 | Example: 34 | 1. Type the following command: [...] 35 | 2. etc. 36 | validations: 37 | required: true 38 | - type: textarea 39 | attributes: 40 | label: Configuration 41 | placeholder: Command line options and/or configuration file, if any. 42 | validations: 43 | required: true 44 | - type: textarea 45 | attributes: 46 | label: Environment 47 | description: | 48 | Enter the following command in a terminal and copy/paste its output: 49 | ```bash 50 | npx envinfo --system --binaries --browsers --npmPackages string-byte-length 51 | ``` 52 | validations: 53 | required: true 54 | - type: checkboxes 55 | attributes: 56 | label: Pull request (optional) 57 | description: 58 | Pull requests are welcome! If you would like to help us fix this bug, 59 | please check our [contributions 60 | guidelines](../blob/main/CONTRIBUTING.md). 61 | options: 62 | - label: I can submit a pull request. 63 | required: false 64 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_types.yml: -------------------------------------------------------------------------------- 1 | name: Bug report (TypeScript types) 2 | description: Report a bug about TypeScript types 3 | title: Please replace with a clear and descriptive title 4 | labels: [bug] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Thanks for reporting this bug! 9 | - type: checkboxes 10 | attributes: 11 | label: Guidelines 12 | options: 13 | - label: 14 | Please search other issues to make sure this bug has not already 15 | been reported. 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: Describe the bug 20 | placeholder: A clear and concise description of what the bug is. 21 | validations: 22 | required: true 23 | - type: textarea 24 | attributes: 25 | label: Steps to reproduce 26 | description: | 27 | Please reproduce the bug using the [TypeScript playground](https://www.typescriptlang.org/play) or [Bug workbench](https://www.typescriptlang.org/dev/bug-workbench), then paste the URL here. 28 | validations: 29 | required: true 30 | - type: textarea 31 | attributes: 32 | label: Environment 33 | description: | 34 | Enter the following command in a terminal and copy/paste its output: 35 | ```bash 36 | npx envinfo --system --binaries --browsers --npmPackages string-byte-length,typescript --npmGlobalPackages typescript 37 | ``` 38 | validations: 39 | required: true 40 | - type: checkboxes 41 | attributes: 42 | label: Pull request (optional) 43 | description: 44 | Pull requests are welcome! If you would like to help us fix this bug, 45 | please check our [contributions 46 | guidelines](../blob/main/CONTRIBUTING.md). 47 | options: 48 | - label: I can submit a pull request. 49 | required: false 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | title: Please replace with a clear and descriptive title 4 | labels: [enhancement] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Thanks for suggesting a new feature! 9 | - type: checkboxes 10 | attributes: 11 | label: Guidelines 12 | options: 13 | - label: 14 | Please search other issues to make sure this feature has not already 15 | been requested. 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: Which problem is this feature request solving? 20 | placeholder: I'm always frustrated when [...] 21 | validations: 22 | required: true 23 | - type: textarea 24 | attributes: 25 | label: Describe the solution you'd like 26 | placeholder: This could be fixed by [...] 27 | validations: 28 | required: true 29 | - type: checkboxes 30 | attributes: 31 | label: Pull request (optional) 32 | description: 33 | Pull requests are welcome! If you would like to help us fix this bug, 34 | please check our [contributions 35 | guidelines](../blob/main/CONTRIBUTING.md). 36 | options: 37 | - label: I can submit a pull request. 38 | required: false 39 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 🎉 Thanks for sending this pull request! 🎉 2 | 3 | Please make sure the title is clear and descriptive. 4 | 5 | If you are fixing a typo or documentation, please skip these instructions. 6 | 7 | Otherwise please fill in the sections below. 8 | 9 | **Which problem is this pull request solving?** 10 | 11 | Example: I'm always frustrated when [...] 12 | 13 | **List other issues or pull requests related to this problem** 14 | 15 | Example: This fixes #5012 16 | 17 | **Describe the solution you've chosen** 18 | 19 | Example: I've fixed this by [...] 20 | 21 | **Describe alternatives you've considered** 22 | 23 | Example: Another solution would be [...] 24 | 25 | **Checklist** 26 | 27 | Please add a `x` inside each checkbox: 28 | 29 | - [ ] I have read the [contribution guidelines](../blob/main/CONTRIBUTING.md). 30 | - [ ] I have added tests (we are enforcing 100% test coverage). 31 | - [ ] I have added documentation in the `README.md`, the `docs` directory (if 32 | any) 33 | - [ ] The status checks are successful (continuous integration). Those can be 34 | seen below. 35 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push, pull_request] 3 | jobs: 4 | combinations: 5 | uses: ehmicky/dev-tasks/.github/workflows/build.yml@main 6 | secrets: inherit 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | npm-debug.log 4 | node_modules 5 | /core 6 | .eslintcache 7 | .lycheecache 8 | .npmrc 9 | .yarn-error.log 10 | !.github/ 11 | /coverage 12 | /build 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 3.0.1 2 | 3 | ## Documentation 4 | 5 | - Improve documentation in `README.md` 6 | 7 | # 3.0.0 8 | 9 | ## Breaking changes 10 | 11 | - Minimal supported Node.js version is now `18.18.0` 12 | 13 | # 2.0.0 14 | 15 | ## Breaking changes 16 | 17 | - Minimal supported Node.js version is now `16.17.0` 18 | 19 | # 1.6.0 20 | 21 | ## Features 22 | 23 | - Improve tree-shaking support 24 | 25 | # 1.5.0 26 | 27 | ## Features 28 | 29 | - Add browser support 30 | 31 | # 1.4.1 32 | 33 | ## Bug fixes 34 | 35 | - Fix `package.json` 36 | 37 | # 1.4.0 38 | 39 | - Switch to MIT license 40 | 41 | # 1.3.0 42 | 43 | ## Features 44 | 45 | - Reduce npm package size 46 | 47 | # 1.2.0 48 | 49 | ## Documentation 50 | 51 | - Add types documentation 52 | 53 | # 1.1.0 54 | 55 | ## Features 56 | 57 | - Improve performance 58 | 59 | # 1.0.1 60 | 61 | Initial release. 62 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | This text is available in 4 | [many other languages](https://www.contributor-covenant.org/translations). 5 | 6 | ## Our Pledge 7 | 8 | We as members, contributors, and leaders pledge to make participation in our 9 | community a harassment-free experience for everyone, regardless of age, body 10 | size, visible or invisible disability, ethnicity, sex characteristics, gender 11 | identity and expression, level of experience, education, socio-economic status, 12 | nationality, personal appearance, race, religion, or sexual identity and 13 | orientation. 14 | 15 | We pledge to act and interact in ways that contribute to an open, welcoming, 16 | diverse, inclusive, and healthy community. 17 | 18 | ## Our Standards 19 | 20 | Examples of behavior that contributes to a positive environment for our 21 | community include: 22 | 23 | - Demonstrating empathy and kindness toward other people 24 | - Being respectful of differing opinions, viewpoints, and experiences 25 | - Giving and gracefully accepting constructive feedback 26 | - Accepting responsibility and apologizing to those affected by our mistakes, 27 | and learning from the experience 28 | - Focusing on what is best not just for us as individuals, but for the overall 29 | community 30 | 31 | Examples of unacceptable behavior include: 32 | 33 | - The use of sexualized language or imagery, and sexual attention or advances of 34 | any kind 35 | - Trolling, insulting or derogatory comments, and personal or political attacks 36 | - Public or private harassment 37 | - Publishing others' private information, such as a physical or email address, 38 | without their explicit permission 39 | - Other conduct which could reasonably be considered inappropriate in a 40 | professional setting 41 | 42 | ## Enforcement Responsibilities 43 | 44 | Community leaders are responsible for clarifying and enforcing our standards of 45 | acceptable behavior and will take appropriate and fair corrective action in 46 | response to any behavior that they deem inappropriate, threatening, offensive, 47 | or harmful. 48 | 49 | Community leaders have the right and responsibility to remove, edit, or reject 50 | comments, commits, code, wiki edits, issues, and other contributions that are 51 | not aligned to this Code of Conduct, and will communicate reasons for moderation 52 | decisions when appropriate. 53 | 54 | ## Scope 55 | 56 | This Code of Conduct applies within all community spaces, and also applies when 57 | an individual is officially representing the community in public spaces. 58 | Examples of representing our community include using an official e-mail address, 59 | posting via an official social media account, or acting as an appointed 60 | representative at an online or offline event. 61 | 62 | ## Enforcement 63 | 64 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 65 | reported to the community leaders responsible for enforcement at 66 | ehmicky+report@gmail.com All complaints will be reviewed and investigated 67 | promptly and fairly. 68 | 69 | All community leaders are obligated to respect the privacy and security of the 70 | reporter of any incident. 71 | 72 | ## Enforcement Guidelines 73 | 74 | Community leaders will follow these Community Impact Guidelines in determining 75 | the consequences for any action they deem in violation of this Code of Conduct: 76 | 77 | ### 1. Correction 78 | 79 | **Community Impact**: Use of inappropriate language or other behavior deemed 80 | unprofessional or unwelcome in the community. 81 | 82 | **Consequence**: A private, written warning from community leaders, providing 83 | clarity around the nature of the violation and an explanation of why the 84 | behavior was inappropriate. A public apology may be requested. 85 | 86 | ### 2. Warning 87 | 88 | **Community Impact**: A violation through a single incident or series of 89 | actions. 90 | 91 | **Consequence**: A warning with consequences for continued behavior. No 92 | interaction with the people involved, including unsolicited interaction with 93 | those enforcing the Code of Conduct, for a specified period of time. This 94 | includes avoiding interactions in community spaces as well as external channels 95 | like social media. Violating these terms may lead to a temporary or permanent 96 | ban. 97 | 98 | ### 3. Temporary Ban 99 | 100 | **Community Impact**: A serious violation of community standards, including 101 | sustained inappropriate behavior. 102 | 103 | **Consequence**: A temporary ban from any sort of interaction or public 104 | communication with the community for a specified period of time. No public or 105 | private interaction with the people involved, including unsolicited interaction 106 | with those enforcing the Code of Conduct, is allowed during this period. 107 | Violating these terms may lead to a permanent ban. 108 | 109 | ### 4. Permanent Ban 110 | 111 | **Community Impact**: Demonstrating a pattern of violation of community 112 | standards, including sustained inappropriate behavior, harassment of an 113 | individual, or aggression toward or disparagement of classes of individuals. 114 | 115 | **Consequence**: A permanent ban from any sort of public interaction within the 116 | community. 117 | 118 | ## Attribution 119 | 120 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 121 | version 2.0, available at 122 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 123 | 124 | Community Impact Guidelines were inspired by 125 | [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 126 | 127 | [homepage]: https://www.contributor-covenant.org 128 | 129 | For answers to common questions about this code of conduct, see the FAQ at 130 | https://www.contributor-covenant.org/faq. Translations are available at 131 | https://www.contributor-covenant.org/translations. 132 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributions 2 | 3 | 🎉 Thanks for considering contributing to this project! 🎉 4 | 5 | These guidelines will help you send a pull request. 6 | 7 | If you're submitting an issue instead, please skip this document. 8 | 9 | If your pull request is related to a typo or the documentation being unclear, 10 | please click on the relevant page's `Edit` button (pencil icon) and directly 11 | suggest a correction instead. 12 | 13 | This project was made with ❤️. The simplest way to give back is by starring and 14 | sharing it online. 15 | 16 | Everyone is welcome regardless of personal background. We enforce a 17 | [Code of conduct](CODE_OF_CONDUCT.md) in order to promote a positive and 18 | inclusive environment. 19 | 20 | # Development process 21 | 22 | First fork and clone the repository. If you're not sure how to do this, please 23 | watch 24 | [these videos](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github). 25 | 26 | Run: 27 | 28 | ```bash 29 | npm install 30 | ``` 31 | 32 | Make sure everything is correctly setup with: 33 | 34 | ```bash 35 | npm test 36 | ``` 37 | 38 | We use Gulp tasks to lint, test and build this project. Please check 39 | [dev-tasks](https://github.com/ehmicky/dev-tasks/blob/main/README.md) to learn 40 | how to use them. You don't need to know Gulp to use these tasks. 41 | 42 | # Requirements 43 | 44 | Our coding style is documented 45 | [here](https://github.com/ehmicky/eslint-config#coding-style). Linting and 46 | formatting should automatically handle it though. 47 | 48 | After submitting the pull request, please make sure the Continuous Integration 49 | checks are passing. 50 | 51 | We enforce 100% test coverage: each line of code must be tested. 52 | 53 | New options, methods, properties, configuration and behavior must be documented 54 | in all of these: 55 | 56 | - the `README.md` 57 | - the `docs` directory (if any) 58 | 59 | Please use the same style as the rest of the documentation and examples. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2025 ehmicky 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Node](https://img.shields.io/badge/-Node.js-808080?logo=node.js&colorA=404040&logoColor=66cc33)](https://www.npmjs.com/package/string-byte-length) 2 | [![Browsers](https://img.shields.io/badge/-Browsers-808080?logo=firefox&colorA=404040)](https://unpkg.com/string-byte-length?module) 3 | [![TypeScript](https://img.shields.io/badge/-Typed-808080?logo=typescript&colorA=404040&logoColor=0096ff)](/src/main.d.ts) 4 | [![Codecov](https://img.shields.io/badge/-Tested%20100%25-808080?logo=codecov&colorA=404040)](https://codecov.io/gh/ehmicky/string-byte-length) 5 | [![Minified size](https://img.shields.io/bundlephobia/minzip/string-byte-length?label&colorA=404040&colorB=808080&logo=webpack)](https://bundlephobia.com/package/string-byte-length) 6 | [![Mastodon](https://img.shields.io/badge/-Mastodon-808080.svg?logo=mastodon&colorA=404040&logoColor=9590F9)](https://fosstodon.org/@ehmicky) 7 | [![Medium](https://img.shields.io/badge/-Medium-808080.svg?logo=medium&colorA=404040)](https://medium.com/@ehmicky) 8 | 9 | Get the UTF-8 byte length of a string. 10 | 11 | # Features 12 | 13 | - [Fastest](#benchmarks) available library in JavaScript. 14 | - Works on [all platforms](#alternatives) (Node.js, browsers, Deno, etc.) 15 | 16 | # Example 17 | 18 | ```js 19 | import stringByteLength from 'string-byte-length' 20 | 21 | stringByteLength('test') // 4 22 | stringByteLength(' ') // 1 23 | stringByteLength('\0') // 1 24 | stringByteLength('±') // 2 25 | stringByteLength('★') // 3 26 | stringByteLength('🦄') // 4 27 | ``` 28 | 29 | # Install 30 | 31 | ```bash 32 | npm install string-byte-length 33 | ``` 34 | 35 | This package works in both Node.js >=18.18.0 and 36 | [browsers](https://raw.githubusercontent.com/ehmicky/dev-tasks/main/src/browserslist). 37 | 38 | This is an ES module. It must be loaded using 39 | [an `import` or `import()` statement](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c), 40 | not `require()`. If TypeScript is used, it must be configured to 41 | [output ES modules](https://www.typescriptlang.org/docs/handbook/esm-node.html), 42 | not CommonJS. 43 | 44 | # Alternatives 45 | 46 | This library uses a mix of multiple algorithms: 47 | 48 | - In Node.js: [`Buffer.byteLength()`](#bufferbytelength) 49 | - Otherwise: 50 | - On big strings: [`TextEncoder`](#textencoder) 51 | - On small strings: [`String.charCodeAt()`](#stringcharcodeat) 52 | 53 | ## String.length 54 | 55 | [`string.length`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length) 56 | retrieves the number of characters (or 57 | ["code units"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length#description)). 58 | This is different from computing the number of bytes when the string is 59 | serialized (for example in a file or network request) since UTF-8 characters can 60 | be 1 to 4 bytes long. 61 | 62 | ## Buffer.byteLength() 63 | 64 | [`Buffer.byteLength(string)`](https://nodejs.org/api/buffer.html#static-method-bufferbytelengthstring-encoding) 65 | is [very fast](#benchmarks) since it uses 66 | [V8's C++ implementation](https://v8.github.io/api/head/classv8_1_1String.html#af99433ee51ed45337e5b4536bd28a834). 67 | However, it only works with Node.js. 68 | 69 | ## Buffer.from() 70 | 71 | [`Buffer.from(string).length`](https://nodejs.org/api/buffer.html#static-method-bufferfromstring-encoding) 72 | has similar pros/cons as [`Buffer.byteLength`](#bufferbytelength) but is 73 | [slower](#benchmarks). 74 | 75 | ## TextEncoder 76 | 77 | [`new TextEncoder().encode(string)`](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encode) 78 | is fast as it relies on lower-level code. However, it is slower on small 79 | strings. 80 | 81 | Also, while widely supported, [a few platforms](https://caniuse.com/textencoder) 82 | (like Opera mini) might still miss it. 83 | 84 | ## Blob 85 | 86 | [`new Blob([string]).size`](https://developer.mozilla.org/en-US/docs/Web/API/Blob/size) 87 | has similar pros/cons as [`TextEncoder`](#textencoder) but is 88 | [slower](#benchmarks). 89 | 90 | ## String.charCodeAt() 91 | 92 | [`String.charCodeAt()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt) 93 | can be used on each character to compute its UTF-8 byte length based on the 94 | resulting codepoint. 95 | 96 | This works on all platforms and is fast on small strings. However, it is slower 97 | than other methods on big strings. 98 | 99 | ## String.codePointAt() 100 | 101 | [`String.codePointAt()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/codePointAt) 102 | has similar props/cons as [`String.charCodeAt()`](#stringcharcodeat) but is 103 | [slower](#benchmarks). 104 | 105 | ## encodeURI() 106 | 107 | [`encodeURI(string)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI) 108 | or 109 | [`encodeURIComponent(string)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) 110 | can be used by counting `%` sequences separately, such as 111 | `encodeURI(string).split(/%..|./u).length - 1`. However, this method is 112 | [very slow](#benchmarks). 113 | 114 | # Benchmarks 115 | 116 | ``` 117 | String length 118 | 0 1 1e1 1e2 1e3 1e4 1e5 1e6 1e7 1e8 119 | Only ASCII Buffer.byteLength() 15.39ns 20.12ns 24.9ns 35.4ns 108ns 817ns 9.76μs 85.1μs 833μs 10.1ms 120 | String.charCodeAt() 3.32ns 6.20ns 22.3ns 163.0ns 1440ns 14200ns 143.00μs 1440.0μs 23600μs 236.0ms 121 | String.codePointAt() 2.72ns 5.20ns 33.5ns 252.0ns 2550ns 23800ns 239.00μs 2380.0μs 23000μs 330.0ms 122 | TextEncoder 116.98ns 123.29ns 123.0ns 129.0ns 169ns 475ns 15.60μs 186.0μs 2520μs 32.5ms 123 | Buffer.from() 36.89ns 146.34ns 175.0ns 227.2ns 432ns 3944ns 48.10μs 271.5μs 3191μs 53.0ms 124 | Blob 9003.03ns 9503.59ns 12311.6ns 9821.1ns 13836ns 21288ns 78.77μs 299.3μs 3029μs 74.9ms 125 | encodeURIComponent() 36.60ns 52.06ns 96.8ns 390.2ns 3257ns 33663ns 323.74μs 3427.8μs 34392μs 358.7ms 126 | encodeURI() 35.91ns 78.32ns 341.2ns 2861.3ns 27141ns 278114ns 3538.72μs 34851.7μs 382012μs 3583.3ms 127 | 128 | Mostly ASCII Buffer.byteLength() 15.39ns 22.9ns 28.4ns 79.5ns 453ns 4.34μs 42.9μs 426μs 4.54ms 44.6ms 129 | Very few non-ASCII String.charCodeAt() 3.32ns 8.1ns 27.6ns 190.0ns 1680ns 16.50μs 161.0μs 1640μs 26.90ms 267.0ms 130 | String.codePointAt() 2.72ns 6.8ns 34.3ns 258.0ns 3630ns 34.60μs 328.0μs 3390μs 33.10ms 329.0ms 131 | TextEncoder 116.98ns 122.0ns 136.0ns 218.0ns 948ns 8.32μs 93.5μs 920μs 9.71ms 103.0ms 132 | Buffer.from() 36.89ns 149.0ns 186.2ns 326.9ns 1901ns 20.89μs 195.4μs 1907μs 18.24ms 193.3ms 133 | Blob 9003.03ns 10037.0ns 9659.6ns 9747.9ns 11271ns 32.85μs 190.6μs 1798μs 14.48ms 173.5ms 134 | encodeURIComponent() 36.60ns 242.8ns 273.8ns 650.8ns 4351ns 43.17μs 405.3μs 4311μs 43.29ms 467.9ms 135 | encodeURI() 35.91ns 256.0ns 492.2ns 3065.5ns 28590ns 288.46μs 3575.2μs 35704μs 365.33ms 3493.0ms 136 | 137 | Mostly ASCII Buffer.byteLength() 15.39ns 22.9ns 28.4ns 101ns 807ns 7.55μs 75.7μs 735μs 7.64ms 77.0ms 138 | Some non-ASCII String.charCodeAt() 3.32ns 8.1ns 25.9ns 208ns 1910ns 18.40μs 183.0μs 1850μs 29.8ms 294.0ms 139 | String.codePointAt() 2.72ns 6.8ns 31.8ns 281ns 2770ns 26.20μs 263.0μs 2620μs 27.0ms 365.0ms 140 | TextEncoder 116.98ns 122.0ns 132.0ns 248ns 1400ns 12.80μs 138.0μs 1370μs 14.40ms 152.0ms 141 | Buffer.from() 36.89ns 149.0ns 186.2ns 391ns 2758ns 33.08μs 309.9μs 2815μs 29.79ms 308.2ms 142 | Blob 9003.03ns 10037.0ns 9659.6ns 12400ns 16406ns 41.63μs 277.1μs 2293μs 22.84ms 267.5ms 143 | encodeURIComponent() 36.60ns 242.8ns 273.8ns 1865ns 17458ns 168.61μs 1886.2μs 24200μs 411ms n/a 144 | encodeURI() 35.91ns 256.0ns 492.2ns 4216ns 41403ns 404.00μs 5227.7μs 52100μs 466ms n/a 145 | 146 | Only non-ASCII Buffer.byteLength() 15.39ns 22.9ns 53.5ns 373ns 3.59μs 35.2μs 380μs 3.81ms 35.6ms 351ms 147 | String.charCodeAt() 3.32ns 8.1ns 45.5ns 404ns 3.69μs 38.2μs 361μs 3.69ms 51.1ms 517ms 148 | String.codePointAt() 2.72ns 6.8ns 44.7ns 407ns 3.92μs 38.9μs 389μs 4.18ms 63.9ms 638ms 149 | TextEncoder 116.98ns 122.0ns 168.0ns 597ns 4.62μs 44.5μs 470μs 4.79ms 53.9ms 525ms 150 | Buffer.from() 36.89ns 149.0ns 243.1ns 1047ns 9.91μs 129.3μs 1328μs 12.97ms 123.5ms 1229ms 151 | Blob 9003.03ns 10037.0ns 9639.7ns 15341ns 25.18μs 114.5μs 1005μs 9.61ms 109.4ms 979ms 152 | encodeURIComponent() 36.60ns 242.8ns 1572.9ns 14175ns 132.62μs 1477.7μs 23932μs 448.94ms 3890.0ms n/a 153 | encodeURI() 35.91ns 256.0ns 1451.3ns 14172ns 132.73μs 1619.8μs 18343μs 177.13ms 1670.0ms n/a 154 | ``` 155 | 156 | # Related projects 157 | 158 | - [`string-byte-slice`](https://github.com/ehmicky/string-byte-slice): Like 159 | `string.slice()` but bytewise 160 | - [`truncate-json`](https://github.com/ehmicky/truncate-json): Truncate a JSON 161 | string 162 | 163 | # Support 164 | 165 | For any question, _don't hesitate_ to [submit an issue on GitHub](../../issues). 166 | 167 | Everyone is welcome regardless of personal background. We enforce a 168 | [Code of conduct](CODE_OF_CONDUCT.md) in order to promote a positive and 169 | inclusive environment. 170 | 171 | # Contributing 172 | 173 | This project was made with ❤️. The simplest way to give back is by starring and 174 | sharing it online. 175 | 176 | If the documentation is unclear or has a typo, please click on the page's `Edit` 177 | button (pencil icon) and suggest a correction. 178 | 179 | If you would like to help us fix a bug or add a new feature, please check our 180 | [guidelines](CONTRIBUTING.md). Pull requests are welcome! 181 | 182 | 183 | 184 | 185 | 186 | 189 | 190 | -------------------------------------------------------------------------------- /ava.config.js: -------------------------------------------------------------------------------- 1 | export { default } from '@ehmicky/dev-tasks/ava.config.js' 2 | -------------------------------------------------------------------------------- /benchmark/spyd.yml: -------------------------------------------------------------------------------- 1 | precision: 5 2 | reporter: debug 3 | inputs: 4 | character: medium 5 | size: 1e2 6 | # TODO: enable inputs variations 7 | # character: 8 | # simple: simple 9 | # low: low 10 | # medium: medium 11 | # complex: complex 12 | # size: 13 | # 0c: 0 14 | # 1c: 1 15 | # 1e1c: 1e1 16 | # 1e2c: 1e2 17 | # 1e3c: 1e3 18 | # 1e4c: 1e4 19 | # 1e5c: 1e5 20 | # 1e6c: 1e6 21 | # 1e7c: 1e7 22 | # 1e8c: 1e8 23 | titles: 24 | charCodeAt: String.charCodeAt() 25 | codePointAt: String.codePointAt() 26 | bufferByteLength: Buffer.byteLength() 27 | bufferFrom: Buffer.from().length 28 | blob: new Blob().size 29 | textEncoder: new TextEncoder().encode().length 30 | encodePatternOne: encodeURIComponent().match().length 31 | encodePatternTwo: encodeURI().split().length 32 | # TODO: add titles of inputs variations 33 | # simple: Only ASCII 34 | # low: Mostly ASCII, very few non-ASCII 35 | # medium: Mostly ASCII, some non-ASCII 36 | # complex: Only non-ASCII 37 | # 0c: 0 characters 38 | # 1c: 1 characters 39 | # 1e1c: 10 characters 40 | # 1e2c: 100 characters 41 | # 1e3c: 1_000 characters 42 | # 1e4c: 10_000 characters 43 | # 1e5c: 100_000 characters 44 | # 1e6c: 1_000_000 characters 45 | # 1e7c: 10_000_000 characters 46 | # 1e8c: 100_000_000 characters 47 | -------------------------------------------------------------------------------- /benchmark/string.js: -------------------------------------------------------------------------------- 1 | // Retrieve string used as input for benchmarks 2 | export const getString = (character, size) => { 3 | if (character === 'complex') { 4 | return COMPLEX_CHARACTER.repeat(size) 5 | } 6 | 7 | if (character === 'medium') { 8 | return getMediumString(size) 9 | } 10 | 11 | const firstChar = character === 'simple' ? '' : COMPLEX_CHARACTER 12 | return `${firstChar}${SIMPLE_CHARACTER.repeat(size)}` 13 | } 14 | 15 | const getMediumString = (size) => { 16 | if (size === 1) { 17 | return SIMPLE_CHARACTER 18 | } 19 | 20 | const chunksCount = size / CHUNK_SIZE 21 | const chunk = `${COMPLEX_CHARACTER}${SIMPLE_CHARACTER.repeat(CHUNK_SIZE - 1)}` 22 | return chunk.repeat(chunksCount) 23 | } 24 | 25 | const SIMPLE_CHARACTER = 'a' 26 | const COMPLEX_CHARACTER = '\u{10000}' 27 | const CHUNK_SIZE = 10 28 | -------------------------------------------------------------------------------- /benchmark/tasks.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-shadow 2 | import { Blob, Buffer } from 'node:buffer' 3 | 4 | import { getNodeByteLength } from '../src/buffer.js' 5 | import { getCharCodeByteLength } from '../src/char_code.js' 6 | import { createTextEncoderFunc } from '../src/encoder.js' 7 | 8 | import { getString } from './string.js' 9 | 10 | const beforeAll = ({ character, size }) => { 11 | // eslint-disable-next-line fp/no-mutation 12 | string = getString(character, size) 13 | } 14 | 15 | // eslint-disable-next-line fp/no-let 16 | let string = '' 17 | 18 | export const charCodeAt = { 19 | beforeAll, 20 | main: () => { 21 | getCharCodeByteLength(string) 22 | }, 23 | } 24 | 25 | export const codePointAt = { 26 | beforeAll, 27 | // Uses imperative code for performance. 28 | /* eslint-disable max-statements, fp/no-let, fp/no-loops, 29 | max-depth, fp/no-mutation */ 30 | main: () => { 31 | const charLength = string.length 32 | let byteLength = 0 33 | 34 | for (let charIndex = 0; charIndex < charLength; charIndex += 1) { 35 | const codepoint = string.codePointAt(charIndex) 36 | 37 | if (codepoint < 0x80) { 38 | byteLength += 1 39 | } else if (codepoint < 0x8_00) { 40 | byteLength += 2 41 | } else if (codepoint < 0x1_00_00) { 42 | byteLength += 3 43 | } else { 44 | byteLength += 4 45 | charIndex += 1 46 | } 47 | } 48 | 49 | return byteLength 50 | }, 51 | /* eslint-enable max-statements, fp/no-let, fp/no-loops, 52 | max-depth, fp/no-mutation */ 53 | } 54 | 55 | export const bufferByteLength = { 56 | beforeAll, 57 | main: () => { 58 | getNodeByteLength(string) 59 | }, 60 | } 61 | 62 | const textEncode = createTextEncoderFunc() 63 | export const textEncoder = { 64 | beforeAll, 65 | main: () => { 66 | textEncode(string) 67 | }, 68 | } 69 | 70 | export const bufferFrom = { 71 | beforeAll, 72 | main: () => { 73 | Buffer.from(string).length 74 | }, 75 | } 76 | 77 | export const blob = { 78 | beforeAll, 79 | main: () => { 80 | new Blob([string]).size 81 | }, 82 | } 83 | 84 | export const encodePatternOne = { 85 | beforeAll, 86 | main: () => { 87 | const matches = encodeURIComponent(string).match(ENCODE_REGEXP_ONE) 88 | string.length + (matches ? matches.length : 0) 89 | }, 90 | } 91 | 92 | const ENCODE_REGEXP_ONE = /%[89ABab]/gu 93 | 94 | export const encodePatternTwo = { 95 | beforeAll, 96 | main: () => { 97 | encodeURI(string).split(ENCODE_REGEXP_TWO).length - 1 98 | }, 99 | } 100 | 101 | const ENCODE_REGEXP_TWO = /%..|./u 102 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | export { default } from '@ehmicky/eslint-config' 2 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | export * from '@ehmicky/dev-tasks' 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "string-byte-length", 3 | "version": "3.0.1", 4 | "type": "module", 5 | "exports": { 6 | "types": "./build/src/main.d.ts", 7 | "default": "./build/src/main.js" 8 | }, 9 | "main": "./build/src/main.js", 10 | "types": "./build/src/main.d.ts", 11 | "files": [ 12 | "build/src/**/*.{js,json,d.ts}", 13 | "!build/src/**/*.test.js", 14 | "!build/src/{helpers,fixtures}" 15 | ], 16 | "sideEffects": false, 17 | "scripts": { 18 | "test": "gulp test" 19 | }, 20 | "description": "Get the UTF-8 byte length of a string", 21 | "keywords": [ 22 | "binary", 23 | "bytes", 24 | "emoji", 25 | "encoding", 26 | "javascript", 27 | "json", 28 | "length", 29 | "library", 30 | "nodejs", 31 | "parsing", 32 | "serialization", 33 | "size", 34 | "string", 35 | "string-manipulation", 36 | "stringify", 37 | "typescript", 38 | "ucs-2", 39 | "unicode", 40 | "utf-16", 41 | "utf-8" 42 | ], 43 | "license": "MIT", 44 | "homepage": "https://www.github.com/ehmicky/string-byte-length", 45 | "repository": { 46 | "type": "git", 47 | "url": "git+https://github.com/ehmicky/string-byte-length.git" 48 | }, 49 | "bugs": { 50 | "url": "https://github.com/ehmicky/string-byte-length/issues" 51 | }, 52 | "author": "ehmicky (https://github.com/ehmicky)", 53 | "directories": { 54 | "lib": "src" 55 | }, 56 | "devDependencies": { 57 | "@ehmicky/dev-tasks": "^3.0.34", 58 | "@ehmicky/eslint-config": "^20.0.32", 59 | "@ehmicky/prettier-config": "^1.0.6", 60 | "spyd": "^0.8.4", 61 | "test-each": "^7.0.1" 62 | }, 63 | "engines": { 64 | "node": ">=18.18.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | export { default } from '@ehmicky/prettier-config' 2 | -------------------------------------------------------------------------------- /src/buffer.js: -------------------------------------------------------------------------------- 1 | // This is the fastest method. However, it is only available in Node.js. 2 | export const getNodeByteLength = (string) => 3 | // eslint-disable-next-line n/prefer-global/buffer 4 | globalThis.Buffer.byteLength(string) 5 | -------------------------------------------------------------------------------- /src/char_code.js: -------------------------------------------------------------------------------- 1 | // Uses imperative code for performance. 2 | // Uses `string.charCodeAt()` over `String.codePointAt()` because it is faster. 3 | /* eslint-disable max-statements, fp/no-let, fp/no-loops, max-depth, 4 | fp/no-mutation, no-continue, unicorn/prefer-code-point */ 5 | export const getCharCodeByteLength = (string) => { 6 | const charLength = string.length 7 | let byteLength = charLength 8 | 9 | for (let charIndex = 0; charIndex < charLength; charIndex += 1) { 10 | const codepoint = string.charCodeAt(charIndex) 11 | 12 | if (codepoint <= LAST_ASCII_CODEPOINT) { 13 | continue 14 | } 15 | 16 | if (codepoint <= LAST_TWO_BYTES_CODEPOINT) { 17 | byteLength += 1 18 | continue 19 | } 20 | 21 | byteLength += 2 22 | 23 | if (codepoint < FIRST_HIGH_SURROGATE || codepoint > LAST_HIGH_SURROGATE) { 24 | continue 25 | } 26 | 27 | // When out-of-bound, this returns NaN, which is `false` with the 28 | // next condition 29 | const nextCodepoint = string.charCodeAt(charIndex + 1) 30 | 31 | // High surrogates should be followed by low surrogates. 32 | // However, JavaScript strings allow invalid surrogates, which are counted 33 | // as a normal 3-byte character. This should not happen often in real code 34 | // though. 35 | if ( 36 | nextCodepoint < FIRST_LOW_SURROGATE || 37 | nextCodepoint > LAST_LOW_SURROGATE 38 | ) { 39 | continue 40 | } 41 | 42 | charIndex += 1 43 | } 44 | 45 | return byteLength 46 | } 47 | 48 | // Last ASCII character (1 byte) 49 | const LAST_ASCII_CODEPOINT = 0x7f 50 | // Last 2-bytes character 51 | const LAST_TWO_BYTES_CODEPOINT = 0x7_ff 52 | // Others are 3 bytes characters 53 | // However, U+d800 to U+dbff: 54 | // - Followed by U+dc00 to U+dfff -> 4 bytes together (astral character) 55 | // - Otherwise -> 3 bytes (like above) 56 | const FIRST_HIGH_SURROGATE = 0xd8_00 57 | const LAST_HIGH_SURROGATE = 0xdb_ff 58 | const FIRST_LOW_SURROGATE = 0xdc_00 59 | const LAST_LOW_SURROGATE = 0xdf_ff 60 | /* eslint-enable max-statements, fp/no-let, fp/no-loops, max-depth, 61 | fp/no-mutation, no-continue, unicorn/prefer-code-point */ 62 | -------------------------------------------------------------------------------- /src/char_code.test.js: -------------------------------------------------------------------------------- 1 | // Test using `string.charCode()` 2 | 3 | // Simulate platforms where neither `Buffer` nor `TextEncoder` is available 4 | // eslint-disable-next-line fp/no-delete, n/prefer-global/buffer, import/unambiguous 5 | delete globalThis.Buffer.byteLength 6 | // eslint-disable-next-line fp/no-delete 7 | delete globalThis.TextEncoder 8 | 9 | await import('./helpers/main.test.js') 10 | -------------------------------------------------------------------------------- /src/encoder.js: -------------------------------------------------------------------------------- 1 | // Initialize the function 2 | export const createTextEncoderFunc = () => 3 | getTextEncoderByteLength.bind(undefined, new TextEncoder()) 4 | 5 | // Compute the byte length using `TextEncoder` 6 | const getTextEncoderByteLength = (textEncoder, string) => { 7 | const encoderBuffer = getBuffer(string) 8 | return textEncoder.encodeInto(string, encoderBuffer).written 9 | } 10 | 11 | // The buffer is cached for performance reason 12 | const getBuffer = (string) => { 13 | const size = string.length * 3 14 | 15 | if (size > CACHE_MAX_MEMORY) { 16 | return new Uint8Array(size) 17 | } 18 | 19 | if (cachedEncoderBuffer === undefined || cachedEncoderBuffer.length < size) { 20 | // eslint-disable-next-line fp/no-mutation 21 | cachedEncoderBuffer = new Uint8Array(size) 22 | } 23 | 24 | return cachedEncoderBuffer 25 | } 26 | 27 | // Maximum amount of memory (in bytes) taken by cached buffer 28 | export const CACHE_MAX_MEMORY = 1e5 29 | // eslint-disable-next-line fp/no-let, init-declarations 30 | let cachedEncoderBuffer 31 | 32 | // `TextEncoder()` is faster once the string is large enough 33 | export const TEXT_ENCODER_MIN_LENGTH = 1e2 34 | -------------------------------------------------------------------------------- /src/encoder.test.js: -------------------------------------------------------------------------------- 1 | // Test using `TextEncoder`, if supported 2 | 3 | // Simulate browsers where `Buffer` is not available 4 | // eslint-disable-next-line fp/no-delete, n/prefer-global/buffer, import/unambiguous 5 | delete globalThis.Buffer.byteLength 6 | 7 | await import('./helpers/main.test.js') 8 | -------------------------------------------------------------------------------- /src/helpers/main.test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line ava/no-ignored-test-files 2 | import test from 'ava' 3 | import { each } from 'test-each' 4 | 5 | import { ALL_STRINGS } from './strings.test.js' 6 | 7 | import stringByteLength from 'string-byte-length' 8 | 9 | each(ALL_STRINGS, ({ title }, { string, size }) => { 10 | test(`Should compute the byte length | ${title}`, (t) => { 11 | t.is(stringByteLength(string), size) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/helpers/strings.test.js: -------------------------------------------------------------------------------- 1 | import { CACHE_MAX_MEMORY, TEXT_ENCODER_MIN_LENGTH } from '../encoder.js' 2 | 3 | // All characters being tested 4 | const CHARACTERS = [ 5 | // ASCII 6 | { string: '\0', size: 1 }, 7 | { string: '\u0001', size: 1 }, 8 | { string: '\b', size: 1 }, 9 | { string: '\t', size: 1 }, 10 | { string: '\n', size: 1 }, 11 | { string: 'a', size: 1 }, 12 | { string: ' ', size: 1 }, 13 | { string: '\u007F', size: 1 }, 14 | // Non-ASCII nor astral before U+0800 15 | { string: '\u0080', size: 2 }, 16 | { string: '\u07FF', size: 2 }, 17 | // Non-ASCII nor astral since U+0800 18 | { string: '\u0800', size: 3 }, 19 | { string: '\uFFFF', size: 3 }, 20 | // Astral characters 21 | { string: '\uD800\uDC00', size: 4 }, 22 | { string: '\uDBFF\uDFFF', size: 4 }, 23 | { string: '\u{10000}', size: 4 }, 24 | { string: '\u{1FFFF}', size: 4 }, 25 | { string: '\u{FFFFF}', size: 4 }, 26 | // Invalid surrogate pairs 27 | { string: '\uD800', size: 3 }, 28 | { string: '\uDBFF', size: 3 }, 29 | { string: '\uDC00', size: 3 }, 30 | { string: '\uDFFF', size: 3 }, 31 | { string: '\uDC00\uD800', size: 6 }, 32 | ] 33 | 34 | // Try each character with prepended|appended characters 35 | const LONG_SPACE = '_'.repeat(TEXT_ENCODER_MIN_LENGTH) 36 | const VERY_LONG_SPACE = '_'.repeat(Math.ceil(CACHE_MAX_MEMORY / 3)) 37 | const STRINGS = CHARACTERS.flatMap(({ string, size }) => [ 38 | { string, size }, 39 | { string: `${string} `, size: size + 1 }, 40 | { string: ` ${string}`, size: size + 1 }, 41 | { string: `${string}${LONG_SPACE}`, size: size + LONG_SPACE.length }, 42 | { string: `${LONG_SPACE}${string}`, size: size + LONG_SPACE.length }, 43 | { 44 | string: `${string}${VERY_LONG_SPACE}`, 45 | size: size + VERY_LONG_SPACE.length, 46 | }, 47 | { 48 | string: `${VERY_LONG_SPACE}${string}`, 49 | size: size + VERY_LONG_SPACE.length, 50 | }, 51 | ]) 52 | 53 | // Also test empty strings 54 | export const ALL_STRINGS = [{ string: '', size: 0 }, ...STRINGS] 55 | -------------------------------------------------------------------------------- /src/main.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the UTF-8 byte length of a string. 3 | * 4 | * @example 5 | * ```js 6 | * stringByteLength('test') // 4 7 | * stringByteLength(' ') // 1 8 | * stringByteLength('\0') // 1 9 | * stringByteLength('±') // 2 10 | * stringByteLength('★') // 3 11 | * stringByteLength('🦄') // 4 12 | * ``` 13 | */ 14 | export default function stringByteLength( 15 | string: T, 16 | ): T extends '' ? 0 : number 17 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { getNodeByteLength } from './buffer.js' 2 | import { getCharCodeByteLength } from './char_code.js' 3 | import { createTextEncoderFunc, TEXT_ENCODER_MIN_LENGTH } from './encoder.js' 4 | 5 | // Retrieve the best method based on the platform support 6 | const getMainFunction = () => { 7 | // eslint-disable-next-line n/prefer-global/buffer 8 | if ('Buffer' in globalThis && 'byteLength' in globalThis.Buffer) { 9 | return getNodeByteLength 10 | } 11 | 12 | if ('TextEncoder' in globalThis) { 13 | return getByteLength.bind(undefined, createTextEncoderFunc()) 14 | } 15 | 16 | return getCharCodeByteLength 17 | } 18 | 19 | const getByteLength = (getTextEncoderByteLength, string) => 20 | string.length < TEXT_ENCODER_MIN_LENGTH 21 | ? getCharCodeByteLength(string) 22 | : getTextEncoderByteLength(string) 23 | 24 | export default getMainFunction() 25 | -------------------------------------------------------------------------------- /src/main.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectNotType, expectType } from 'tsd' 2 | 3 | import stringByteLength from 'string-byte-length' 4 | 5 | expectType<0>(stringByteLength('')) 6 | expectNotType<0>(stringByteLength('a')) 7 | expectType(stringByteLength('a')) 8 | // @ts-expect-error 9 | stringByteLength(true) 10 | -------------------------------------------------------------------------------- /src/main.test.js: -------------------------------------------------------------------------------- 1 | // Test using the current platform's preferred method 2 | 3 | // eslint-disable-next-line import/no-unassigned-import 4 | import './helpers/main.test.js' 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ehmicky/dev-tasks/tsconfig.json" 3 | } 4 | --------------------------------------------------------------------------------