├── .editorconfig ├── src ├── index.ts ├── util.ts ├── specifiler.ts └── formatter.ts ├── .npmignore ├── @types └── uck │ └── index.d.ts ├── tsconfig.json ├── package.json ├── LICENSE ├── .gitignore ├── test └── formatter.spec.ts ├── karma.conf.js └── README.md /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_size = 4 3 | indent_stype = space -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { format, convert } from './formatter'; 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_store 2 | .git* 3 | docs/ 4 | examples/ 5 | test/ 6 | src/ 7 | .editorconfig 8 | coverage 9 | karma.conf.js -------------------------------------------------------------------------------- /@types/uck/index.d.ts: -------------------------------------------------------------------------------- 1 | export = uck; 2 | export as namespace uck; 3 | 4 | declare namespace uck { 5 | 6 | /** 7 | * specifier에 따라 format 해주는 function을 반환한다. 8 | */ 9 | export function format(specifier: string): (value: number) => string; 10 | 11 | /** 12 | * specifier에 맞춰서 value를 format해준다. 13 | */ 14 | export function convert(specifier: string, value: number): string; 15 | } -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | export function isDigit(c: string) { 2 | return !isNaN(parseInt(c)); 3 | } 4 | 5 | export function getDigitLength(value: number) { 6 | if (value === 0) { 7 | return 1; 8 | } 9 | return Math.floor(Math.log10(value)) + 1; 10 | } 11 | 12 | export function isFixType(type: string) { 13 | return type[0] === 'f'; 14 | } 15 | 16 | export function rawString(value: number) { 17 | return value.toLocaleString('fullwide', { useGrouping: false }); 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitAny": true, 5 | "removeComments": true, 6 | "preserveConstEnums": true, 7 | "downlevelIteration": true, 8 | "sourceMap": true, 9 | "outDir": "dist", 10 | "lib": ["es2017", "dom"], 11 | "typeRoots": ["@types"], 12 | }, 13 | "include": [ 14 | "src/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "node_modules", 18 | "src/*.spec.ts", 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uck", 3 | "version": "1.0.0", 4 | "description": "Format numbers for Korean", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "karma start ./karma.conf.js --browsers Chrome" 8 | }, 9 | "types": "@types/uck/index.d.ts", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/JunhoeKim/uck.git" 13 | }, 14 | "keywords": [ 15 | "formatter", 16 | "korean", 17 | "format", 18 | "d3" 19 | ], 20 | "author": "Junhoe Kim", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/JunhoeKim/d3-korean/issues" 24 | }, 25 | "homepage": "https://github.com/JunhoeKim/uck#readme", 26 | "devDependencies": { 27 | "@types/jasmine": "^3.3.9", 28 | "@types/karma": "^3.0.2", 29 | "jasmine-core": "^3.3.0", 30 | "karma": "^4.0.1", 31 | "karma-chrome-launcher": "^2.2.0", 32 | "karma-jasmine": "^2.0.1", 33 | "karma-typescript": "^4.0.0", 34 | "typescript": "^3.2.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 JunhoeKim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /src/specifiler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Specifier가 감지하는 form은 아래와 같다: 4 | * 5 | * [align][width][,][$][.precision][s][~][type] 6 | * 7 | * align: 만들어진 format의 정렬 (<: 왼쪽, ^: 가운데, >: 오른쪽 정렬) 8 | * width: 만들어진 format이 차지하는 총 width, 만약 결과 format이 이 입력값보다 크면 9 | * width값은 무시된다. 10 | * ,: 천 자리마다 ,를 찍는다. 11 | * $: 숫자 뒤에 원을 붙인다. 12 | * precision: 유효숫자를 정한다. 13 | * ~: 의미없는 0 padding을 제거해준다. 14 | * s: 각 suffix마다 space를 만들지 말지를 선택한다. 15 | * type: formatting 타입이다. 16 | * 가능한 type의 종류는 다음과 같다.: 17 | * 18 | * b: 가장 기본적인 type. ex) 1230000 => 123만 19 | * b+: 가장 기본적인 type에서 십, 백, 천 단위의 suffix를 추가 ex) 1230000 => 1백2십3만. 20 | * k: digits가 없는 순수 한글 format. ex) 1230000 => 백이십삼만 21 | * f[suffix]: 단위 suffix를 하나로 고정시킨다. ex) format('~f천')(1230000) => 1230천 22 | * 23 | */ 24 | 25 | export default class FormatSpecifier { 26 | 27 | public align: string; 28 | public width: number; 29 | public comma: boolean; 30 | public currency: boolean; 31 | public precision: number; 32 | public spacing: boolean; 33 | public trim: boolean; 34 | public type: string; 35 | 36 | constructor(specifier: string) { 37 | 38 | const re = /^([<^>])?(\d+)?(,)?(\$)?(\.\d+)?(s)?(~)?((b\+?)|k|f[십백천만억조경해자양])?$/; 39 | const match = re.exec(specifier); 40 | if (!match) { 41 | throw new Error('Invalid format: ' + specifier); 42 | } 43 | this.align = match[1] || ">"; 44 | this.width = match[2] && +match[2]; 45 | this.comma = !!match[3]; 46 | this.currency = !!match[4]; 47 | this.precision = match[5] && +match[5].slice(1); 48 | this.spacing = !!match[6]; 49 | this.trim = !!match[7]; 50 | this.type = match[8] || "b"; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/formatter.spec.ts: -------------------------------------------------------------------------------- 1 | import * as uck from '../src/index'; 2 | 3 | function validateFormat(value: number, format: string, expectedResult: string) { 4 | it('The result should be equal to expected results', () => { 5 | const result = uck.format(format)(value); 6 | expect(result).toBe(expectedResult); 7 | }); 8 | } 9 | 10 | function validateConvert(value: number, format: string, expectedResult: string) { 11 | it('The result should be equal to expected results', () => { 12 | const result = uck.convert(format, value); 13 | expect(result).toBe(expectedResult); 14 | }); 15 | } 16 | 17 | describe('A suite to check wheter the formatter works valid or not', () => { 18 | 19 | validateFormat(12345678, '', '1234만5678'); 20 | validateConvert(12345678, '', '1234만5678'); 21 | 22 | validateFormat(123456780000, '', '1234억5678만'); 23 | validateFormat(-123456780000, '', '-1234억5678만'); 24 | validateFormat(-1, '', '-1'); 25 | validateFormat(100000001, '', '1억1'); 26 | validateFormat(12345678900000000000000, '', '123해4567경8900조'); 27 | validateFormat(12345678, '.2', '1200만'); 28 | validateFormat(12345678, '$.3', '1230만원'); 29 | validateFormat(12345678, '.6s', '1234만 5700'); 30 | validateFormat(12, '.4s', '12.00'); 31 | validateFormat(12, '.4sb+', '1십 2.00'); 32 | validateFormat(1234567890, '.6sb+', '1십 2억 3천 4백 5십 7만'); 33 | validateFormat(1234567890, '.6sf천', '1234570천'); 34 | validateFormat(1230000, '~f천', '1230천'); 35 | validateFormat(1, '~f만', '0.0001만'); 36 | validateFormat(1, 'f만', '0.0001만'); 37 | validateFormat(-0.001, 'f백', '-0.00001백'); 38 | validateFormat(12345678900000000000000, 'sf해', '123.456789해'); 39 | validateFormat(1234567890, ',f천', '1,234,567.89천'); 40 | validateFormat(12345678, ',$.8', '1,234만5,678원'); 41 | validateFormat(12345678, ',$.6s', '1,234만 5,700원'); 42 | validateFormat(12345678, ',$.6f백', '123,457백원'); 43 | validateFormat(12345678, ',$.6b+', '1천2백3십4만5천7백원'); 44 | validateFormat(123456.78, ',.9~b', '12만3,456.78'); 45 | validateFormat(12345, '8,.3', ' 1만2,300'); 46 | validateFormat(12345, '<9,$.3', '1만2,300원 '); 47 | validateFormat(12345, '^12,$.3', ' 1만2,300원 '); 48 | validateFormat(-54321, 'k', '-오만사천삼백이십일'); 49 | validateFormat(12345, '^12,$.3k', ' 만이천삼백원 '); 50 | validateFormat(40000000, 'b+', '4천만') 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sun Mar 10 2019 01:39:38 GMT+0900 (Korean Standard Time) 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine', 'karma-typescript'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'src/**/*.ts', 19 | 'test/**/*.spec.ts' 20 | ], 21 | 22 | 23 | // list of files / patterns to exclude 24 | exclude: [ 25 | ], 26 | 27 | 28 | // preprocess matching files before serving them to the browser 29 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 30 | preprocessors: { 31 | '**/*.ts': ['karma-typescript'] 32 | }, 33 | 34 | 35 | // test results reporter to use 36 | // possible values: 'dots', 'progress' 37 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 38 | reporters: ['progress', 'karma-typescript'], 39 | 40 | 41 | karmaTypescriptConfig: { 42 | compilerOptions: { 43 | downlevelIteration: true, 44 | }, 45 | }, 46 | 47 | // web server port 48 | port: 9876, 49 | 50 | 51 | // enable / disable colors in the output (reporters and logs) 52 | colors: true, 53 | 54 | 55 | // level of logging 56 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 57 | logLevel: config.LOG_INFO, 58 | 59 | 60 | // enable / disable watching file and executing tests whenever any file changes 61 | autoWatch: true, 62 | 63 | 64 | // start these browsers 65 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 66 | browsers: ['Chrome'], 67 | 68 | 69 | // Continuous Integration mode 70 | // if true, Karma captures browsers, runs the tests and exits 71 | singleRun: false, 72 | 73 | // Concurrency level 74 | // how many browser should be started simultaneous 75 | concurrency: Infinity 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uck.js 2 | Format numbers for Korean 3 | 4 | 영어권에서 사용하는 SI 단위계와 한국 사람들이 사용하는 단위 체계는 꽤 다릅니다. 5 | 6 | 1000000 = 1M = 100만 7 | 100000000 = 100M = 1억 8 | 9 | 이런 상황에서 한글 단위계를 직접 구현해서 formatting하는 일은 생각보다 여러분을 귀찮게 할 것입니다. 10 | 11 | 웹 상에서 한글이 포함된 숫자를 formatting하려는 사람들이나 시각화를 제작하면서 축에 한글 단위계를 적용하고 싶은 사람들을 위한 모듈입니다. 12 | 13 | 이 모듈 구현체는 [d3-format](https://github.com/d3/d3-format) repository에서 많은 참고를 했습니다. 14 | 15 | 16 | ```js 17 | for (let i = 10; i <= 15; i++) { 18 | console.log(uck.convert('.2b', i * 1000)); 19 | } 20 | ``` 21 | 22 | 위와 같은 문법을 바탕으로 변환해주면 다음과 같은 결과를 얻을 수 있습니다. 23 | 24 | 1만 25 | 1만1000 26 | 1만2000 27 | 1만3000 28 | 1만4000 29 | 1만5000 30 | 31 | 그 외에도 uck.js는 다양한 format을 지원합니다. 32 | 33 | ```js 34 | uck.convert('$.3', 12345678); // 1230만원 35 | uck.convert(',.12', 12345678); // 1,234만5,678.0000 36 | uck.convert('.4sb+', 12); // 1십 2.00 37 | uck.convert('~f천', 1230000); // 1230천 38 | uck.convert('^12$.3k', 12345); // 만이천삼백원 /// 39 | ``` 40 | 41 | 42 | ## Installing 43 | NPM 모듈로 지원하고 있기 때문에 `npm install uck`를 통하여 설치하실 수 있습니다. 44 | 45 | 46 | ## API reference 47 | 48 | uck.**format**(*specifier*) 49 | 50 | value를 argument로 받을 수 있는 format 함수를 반환합니다. 51 | Specifier에 지정할 수 있는 문법은 다음과 같습니다. 52 | 53 | [align][width][,][$][.precision][s][~][type] 54 | 55 | *align*: 만들어진 format이 어디로 정렬할지를 결정합니다. width와 같이 쓰이고 width가 실제 format의 길이보다 길어야 의미가 있습니다. 지정하지않으면 기존적으로 오른쪽 정렬로 동작합니다. 56 | 57 | * `<` - 왼쪽 정렬 58 | * `>` - 오른쪽 정렬 59 | * `^` - 가운데 정렬 60 | 61 | *width*: 만들어진 format이 차지하는 총 width에 해당합니다. 결과 format이 입력값보다 크면 width 값은 무시됩니다. 62 | 63 | *,*: 매 천 자리가 반복될 때마다 ,를 찍어줍니다. 64 | 65 | *$*: 숫자 뒤에 원을 붙입니다. 66 | 67 | *precision*: 유효숫자를 지정해서 왼쪽부터 precision 수만큼의 숫자가 보존됩니다. precision 값은 **0~20** 까지만 유효합니다. 지정하지 않으면 input value의 길이만큼이 precision으로 결정됩니다. 나머지 단위는 마지막 precision 단위에 맞춰 반올림되서 계산합니다. 68 | 69 | *~*: 소수 자리에서 의미없는 0을 제거해줍니다. 70 | 71 | *s*: 각 한글 단위마다 한 칸 공백을 추가합니다. 72 | 73 | *type*: formatting 타입을 결정하고 다음과 같은 formatting 방법이 있습니다. 74 | 75 | * `b`: 가장 기본적인 format입니다. 만 자리부터 1e4마다 한글 단위가 생기며 **만억조경해자양** 단위까지 지원합니다. type을 지정하지 않으면 default type으로 결정됩니다. 76 | 77 | ```js 78 | uck.format('.5b')(1234560000) // 12억3450만 79 | uck.format('')(100000001) // 1억1 80 | ``` 81 | 82 | * `b+`: 기본적인 format에서 더해져서 **십백천** 단위를 사이사이에 끼워넣은 버전입니다. 83 | 84 | ```js 85 | uck.format('$.6b+')(12345678) // 1천2백3십4만5천7백원 86 | ``` 87 | 88 | * `k`: 모든 숫자 부분을 한글로 변환합니다. 89 | 90 | ```js 91 | uck.format('k')(-54321) // -오만사천삼백이십일 92 | ``` 93 | 94 | * `f`: 원하는 숫자단위를 기준으로 formatting합니다. 95 | 96 | ```js 97 | uck.format('.6sf천')(1234567890) // 1234570천 98 | uck.format('.f만')(1) // 0.0001만 99 | ``` 100 | 101 | uck.**convert**(*specifier*, *value*) 102 | 103 | `uck.format(specifier)(value)`와 똑같이 동작합니다. 104 | 105 | 버그 리포트나 발전 방향에 대한 제안은 언제나 환영합니다. 지속적으로 테스트 케이스를 늘려서 실험해볼 계획입니다. 106 | -------------------------------------------------------------------------------- /src/formatter.ts: -------------------------------------------------------------------------------- 1 | import { default as FormatSpecifier } from './specifiler'; 2 | import { isDigit, getDigitLength, isFixType, rawString } from './util'; 3 | 4 | const subSuffixToValue: { [key: string]: number } = { 5 | '십': 10, 6 | '백': 100, 7 | '천': 1000, 8 | } 9 | 10 | const baseSuffixToValue: { [key: string]: number } = { 11 | '': 1, 12 | '만': 10000, 13 | '억': 1e8, 14 | '조': 1e12, 15 | '경': 1e16, 16 | '해': 1e20, 17 | '자': 1e24, 18 | '양': 1e28, 19 | } 20 | 21 | const digitToKorean: { [key: string]: string } = { 22 | '1': '일', 23 | '2': '이', 24 | '3': '삼', 25 | '4': '사', 26 | '5': '오', 27 | '6': '육', 28 | '7': '칠', 29 | '8': '팔', 30 | '9': '구', 31 | } 32 | 33 | const totalSuffixToValue = { ...baseSuffixToValue, ...subSuffixToValue }; 34 | 35 | class Formatter { 36 | 37 | public specifier: string = ''; 38 | 39 | constructor(specifier: string) { 40 | this.specifier = specifier; 41 | } 42 | 43 | convert(value: number): string { 44 | return this.format(value); 45 | } 46 | 47 | /** 48 | * input value를 specifier맞춰서 formatting한다. 49 | * @param value input value 50 | */ 51 | format(value: number): string { 52 | let { align, width, comma, currency, precision, spacing, trim, type } = new FormatSpecifier(this.specifier); 53 | let positive = value >= 0; 54 | value = Math.abs(value); 55 | 56 | let roundedFormat = this.applyPrecision(value, precision); 57 | let [digits, decimal] = roundedFormat.split('.'); 58 | decimal = decimal || ''; 59 | let format = ''; 60 | 61 | switch (type) { 62 | case 'b': 63 | format = this.addSuffix(digits); 64 | break; 65 | case 'b+': 66 | format = this.addSuffix(digits); 67 | format = this.addSubSuffix(format); 68 | break; 69 | case 'k': 70 | format = this.addSuffix(digits); 71 | format = this.addSubSuffix(format); 72 | format = this.convertToKorean(format); 73 | break; 74 | default: 75 | format = this.fixBaseSuffix(digits, decimal, type.substring(1)); 76 | break; 77 | } 78 | 79 | if (!isFixType(type)) { 80 | if (spacing) { 81 | format = this.applySpacing(format); 82 | } 83 | 84 | if (trim) { 85 | decimal = this.trimDecimal(decimal); 86 | } 87 | format = format + (decimal ? '.' + decimal : ''); 88 | } 89 | 90 | if (comma) { 91 | format = this.addComma(format); 92 | } 93 | 94 | if (currency) { 95 | format = format + '원'; 96 | } 97 | 98 | if (!positive) { 99 | format = '-' + format; 100 | } 101 | 102 | if (width && width > format.length) { 103 | format = this.alignFormat(format, width, align); 104 | } 105 | 106 | return format; 107 | } 108 | 109 | /** 110 | * 기본 type b, b+에서 사용되는 formatting, 111 | * 일반 숫자에 한글을 붙여준다. 112 | * @param digits 정수 부분을 나타내는 숫자 문자열 113 | */ 114 | private addSuffix(digits: string) { 115 | let result = ''; 116 | 117 | // 정수 부분 값이 10 미만이라면 아무것도 안한 결과를 뱉음 118 | if (digits.length <= 1) { 119 | return digits; 120 | } 121 | 122 | const suffixEntries = Object.entries(baseSuffixToValue); 123 | 124 | suffixEntries.forEach(pair => { 125 | const [suffix, value] = pair; 126 | const suffixValueLength = Math.log10(value) + 1; 127 | 128 | if (digits.length >= suffixValueLength) { 129 | // suffix가 '양' 보다 작을 때와 클 때 130 | const endIndex = digits.length - suffixValueLength; 131 | const startIndex = Math.max(0, endIndex - 3); 132 | const chunk = digits.substring(startIndex, endIndex + 1); 133 | const notZeroIndex = [...chunk].findIndex(c => c !== '0'); 134 | result = (notZeroIndex === -1 ? '' : chunk.substring(notZeroIndex) + suffix) + result; 135 | } 136 | }); 137 | return result; 138 | } 139 | 140 | /** 141 | * SubSuffix를 기본 format에 더한다. 142 | * @param format 현재까지 processing 된 format string 143 | */ 144 | private addSubSuffix(format: string): string { 145 | const suffixes = ['', '십', '백', '천']; 146 | let startIndex = 0; 147 | let endIndex = 0; 148 | let result = ''; 149 | // 마지막에 빈 문자열을 하나 추가해서 단위가 없는 1의 자리를 처리한다. 150 | const formatChars = [...format].concat(['']); 151 | formatChars.forEach((c, i) => { 152 | if (!isDigit(c) || i === formatChars.length - 1) { 153 | let chunk = ''; 154 | endIndex = i - 1; 155 | for (let j = endIndex; j >= startIndex; j--) { 156 | chunk = (formatChars[j] !== '0' ? formatChars[j] + suffixes[endIndex - j] : '') + chunk; 157 | } 158 | chunk += c; 159 | startIndex = i + 1; 160 | result += chunk; 161 | } 162 | }); 163 | return result; 164 | } 165 | 166 | /** 167 | * 숫자를 제거하여 한글로만 구성되게 바꿔준다. 168 | * @param format 현재까지 process된 format 169 | */ 170 | public convertToKorean(format: string): string { 171 | return format.replace(/1([십백천만억조경해자양])/g, (_, g) => g).replace(/([1-9])/g, (_, g) => digitToKorean[g]); 172 | } 173 | 174 | /** 175 | * 사용자가 정한 한글 숫자 단위에 맞춰서 format을 재구성한다. 176 | * @param format 현재까지 processing 된 format string 177 | * @param base 기준이 되는 숫자 한글 단위 178 | */ 179 | private fixBaseSuffix(digits: string, decimal: string, base: string): string { 180 | const baseDigitLength = getDigitLength(totalSuffixToValue[base]); 181 | const digitLength = getDigitLength(+digits); 182 | if (baseDigitLength > digitLength) { 183 | const trimmedDecimal = this.trimDecimal('0'.repeat(baseDigitLength - digitLength - 1) + digits + decimal); 184 | return trimmedDecimal ? '0.' + trimmedDecimal + base : 0 + base; 185 | } else { 186 | const pointIndex = digitLength - baseDigitLength + 1; 187 | const trimmedDecimal = this.trimDecimal(digits.substring(pointIndex) + decimal); 188 | return digits.substring(0, pointIndex) + (trimmedDecimal ? '.' + trimmedDecimal : '') + base; 189 | } 190 | } 191 | 192 | /** 193 | * Precision에 따라 반올림을 하거나 소수 점을 붙여서 string으로 바꿔준다. 194 | * @param value input number value 195 | * @param precision precision을 적용할 자릿 수 196 | */ 197 | private applyPrecision(value: number, precision: number) { 198 | const digitLength = rawString(value).replace(/(\.|-)/g, '').length; 199 | if (!precision) { 200 | precision = digitLength; 201 | } 202 | precision = Math.max(0, Math.min(20, precision)); 203 | if (precision > digitLength) { 204 | const result = rawString(value); 205 | return result 206 | + (result.includes('.') ? '' : '.') 207 | + '0'.repeat(precision - digitLength); 208 | } else if (precision === digitLength) { 209 | return rawString(value); 210 | } else { 211 | const precisionValue = Math.pow(10, digitLength - precision); 212 | return rawString(Math.round(value / precisionValue) * precisionValue); 213 | } 214 | } 215 | 216 | /** 217 | * Suffix와 다음 숫자 사이에 빈 칸을 둔다. 218 | * @param format 현재까지 preocessing 된 format string 219 | */ 220 | private applySpacing(format: string): string { 221 | return format.replace(/([십백천만억조경해자양])(\d+)/g, (_, g1, g2) => [g1, g2].join(' ')); 222 | } 223 | 224 | /** 225 | * 천 단위마다 ,를 붙여준다. 226 | * @param format 현재까지 processing된 format string 227 | */ 228 | private addComma(format: string): string { 229 | let offset = 0; 230 | let endIndex = [...format].findIndex(c => c === '.') - 1; 231 | endIndex = endIndex === -1 ? format.length : endIndex; 232 | for (let i = format.length; i >= 0; i--) { 233 | offset = isDigit(format[i]) ? offset + 1 : 0; 234 | if (offset === 4) { 235 | format = format.substring(0, i + 1) + ',' + format.substring(i + 1); 236 | offset = 1; 237 | } 238 | } 239 | return format; 240 | } 241 | 242 | /** 243 | * 소수부분 뒤에서부터 영향이 없는 0을 제거해준다. 244 | * @param decimal 소수 부분 문자열 245 | */ 246 | private trimDecimal(decimal: string): string { 247 | const trimmedLength = [...decimal].reverse().findIndex(c => c !== '0'); 248 | if (trimmedLength === -1) { 249 | return ''; 250 | } 251 | return decimal.substring(0, decimal.length - trimmedLength); 252 | } 253 | 254 | /** 255 | * width, align 값에 따라서 결과를 정렬한다. 256 | * @param format 현재까지 processing된 format string 257 | * @param width format string이 차지할 영역의 길이 258 | * @param align 정렬 방향 (오른쪽, 가운데, 왼쪽) 259 | */ 260 | private alignFormat(format: string, width: number, align: string): string { 261 | switch (align) { 262 | case '>': 263 | return ' '.repeat(width - format.length) + format; 264 | case '^': 265 | const leftMargin = Math.round((width - format.length) / 2); 266 | const rightMargin = width - format.length - leftMargin; 267 | return ' '.repeat(leftMargin) + format + ' '.repeat(rightMargin); 268 | case '<': 269 | return format + ' '.repeat(width - format.length); 270 | } 271 | } 272 | } 273 | 274 | export function format(specifier: string) { 275 | const formatter = new Formatter(specifier); 276 | return formatter.format.bind(formatter); 277 | } 278 | 279 | export function convert(specifier: string, value: number) { 280 | const formatter = new Formatter(specifier); 281 | return formatter.convert(value); 282 | } 283 | --------------------------------------------------------------------------------