Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# Changelog

## 0.6.0 (2026-08-08)

### Added

- **Indian lakh/crore grouping** — `group` now accepts a
`[primary, secondary]` pair, so the last `primary` digits form one group
and everything above is cut every `secondary`: `{ group: [3, 2] }` gives
`12,34,56,789` (and `1,00,000` for a lakh, `1,00,00,000` for a crore).
Available as `data-numkey-group="3,2"` too. A single number still means
uniform grouping (`4` → `1234,5678`), and `group: 0` now turns separators
off instead of producing a pathological regex.
**인도식 lakh/crore 그룹핑** — `group`에 `[primary, secondary]` 쌍 지원
(`12,34,56,789`), 속성은 `data-numkey-group="3,2"`.

### Fixed

- **`locale` now derives group sizes, not just separators.** A field with
`locale: 'en-IN'` (or `data-numkey-locale="en-IN"`) rendered
`123,456,789` — the separator was right but the grouping was not what
that locale uses. It now formats `12,34,56,789`, matching
`Intl.NumberFormat`. Locales with ordinary thousands grouping are
unaffected, and an explicit `group` still wins.
**`locale`가 구분자만 따르고 그룹 크기는 무시하던 문제 수정** — `en-IN`
필드가 이제 `12,34,56,789`로 나옵니다. 다른 로케일은 영향 없음.

### Changed

- `localeSeparators(tag)` additionally returns `group: [primary, secondary]`.
/ `localeSeparators`가 `group`을 함께 반환합니다.

## 0.5.0 (2026-08-08)

### Added
Expand Down
30 changes: 26 additions & 4 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ numkey는 그 인풋을 한 번에 끝냅니다:
| `data-numkey` | **스위치** — 인풋을 바인딩; 값은 최대 소수 자릿수 (빈 값 = 정수) |
| `data-numkey-negative` | 앞자리 마이너스 허용 |
| `data-numkey-align="left"` | 자동 오른쪽 정렬 옵트아웃 |
| `data-numkey-group="4"` | 그룹 크기 (기본 3, 만 단위는 4) |
| `data-numkey-group="4"` | 그룹 크기 (기본 3, 만 단위는 4); 인도식 lakh는 `"3,2"` — 아래 참조 |
| `data-numkey-separator=" "` | 그룹 구분자 (기본 `,`) |
| `data-numkey-point=","` | 필드에 표시되는 소수점 (기본 `.`) |
| `data-numkey-locale` | 로케일에서 구분자 유도 — 아래 참조 |
Expand Down Expand Up @@ -114,6 +114,26 @@ numkey는 그 인풋을 한 번에 끝냅니다:
돌려줍니다. 일반 폼 POST는 *표시 값*을 전송하므로, 로케일을 쓰는 폼은
`data-numkey-name`(아래)으로 전송하거나 서버에서 정규화하세요.

### 인도식 lakh/crore 그룹핑

인도식 표기는 마지막 세 자리를 묶고 그 위로는 두 자리씩 끊습니다 —
`123,456,789`가 아니라 `12,34,56,789`. `group`에 `[primary, secondary]`
쌍을 주거나, 로케일만 지정하면 됩니다:

```html
<input data-numkey data-numkey-group="3,2"> <!-- 1,00,000 (1 lakh) -->
<input data-numkey data-numkey-locale="en-IN"> <!-- 동일, 자동 유도 -->
```

```ts
format('10000000', { group: [3, 2] }) // '1,00,00,000' (1 crore)
format('123456789', { locale: 'en-IN' }) // '12,34,56,789'
```

명시한 `group`이 로케일보다 항상 우선하고, 숫자 하나면 기존처럼 균일
그룹핑입니다(`4` → `1234,5678`, 만 단위). `group: 0`이면 구분자를 아예
넣지 않습니다.

### 한글 금액 병기

은행·핀테크 UI가 금액 필드 옆에 그리는 "150만" 힌트 — 프로젝트마다
Expand Down Expand Up @@ -268,7 +288,7 @@ const [amount, setAmount] = createSignal('') // 정식 값
|---|---|---|
| `decimals` | `0` | 최대 소수 자릿수 (0 = 정수만) |
| `negative` | `false` | 앞자리 마이너스 허용 |
| `group` | `3` | 그룹당 자릿수 (만 단위 그룹핑은 4) |
| `group` | `3` | 그룹당 자릿수 — 만 단위는 `4`, 인도식 lakh는 `[3, 2]`, `0`이면 구분 없음 |
| `separator` | `","` | 표시용 그룹 구분자 |
| `decimalPoint` | `"."` | 표시용 소수점 (정식 값은 항상 `.`) |
| `locale` | — | **옵트인**: `Intl`로 `separator`/`decimalPoint` 유도 — `"auto"`(브라우저 언어) 또는 BCP 47 태그. 지정하지 않으면 방문자 브라우저와 무관하게 표시가 고정됩니다 (업무 폼의 기본 요구). 명시한 `separator`/`decimalPoint`가 우선. |
Expand Down Expand Up @@ -321,8 +341,10 @@ const [amount, setAmount] = createSignal('') // 정식 값
- ~~`v0.3` — 한글 축약 입력 (`3만5천` → 35,000)~~ ✅ 출시됨
- ~~`v0.4` — 구분자 넘는 스마트 삭제 + `min`/`max`~~ ✅ 출시됨
- ~~`v0.5` — Svelte 액션 / Solid 디렉티브~~ ✅ 출시됨
- 인도식 lakh 그룹핑 (`12,34,567` — 비균일 그룹 크기) — 수요가 있으면 진행,
필요하시면 [이슈](https://github.com/devslab-kr/numkey/issues)로 알려주세요
- ~~`v0.6` — 인도식 lakh/crore 그룹핑 (`12,34,567`) + 로케일 기반 그룹 크기~~ ✅ 출시됨

예정된 항목은 없습니다 — 폼에 필요한 기능이 빠져 있다면
[이슈](https://github.com/devslab-kr/numkey/issues)로 알려주세요.

## 기여하기

Expand Down
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ it does nothing:
| `data-numkey` | **the switch** — binds the input; the value is the max decimal places (empty = integer) |
| `data-numkey-negative` | allow a leading minus |
| `data-numkey-align="left"` | opt out of automatic right alignment |
| `data-numkey-group="4"` | group size (default 3) |
| `data-numkey-group="4"` | group size (default 3); `"3,2"` for Indian lakh — see below |
| `data-numkey-separator=" "` | group separator (default `,`) |
| `data-numkey-point=","` | decimal mark shown in the field (default `.`) |
| `data-numkey-locale` | derive separators from a locale — see below |
Expand Down Expand Up @@ -117,6 +117,26 @@ three cases. Since a plain form POST submits the *display* value, a form
using locales should post through `data-numkey-name` (below) or normalize
server-side.

### Indian lakh/crore grouping

The Indian system groups the last three digits and then every two —
`12,34,56,789`, not `123,456,789`. Give `group` a `[primary, secondary]`
pair, or just name the locale:

```html
<input data-numkey data-numkey-group="3,2"> <!-- 1,00,000 (1 lakh) -->
<input data-numkey data-numkey-locale="en-IN"> <!-- same, derived -->
```

```ts
format('10000000', { group: [3, 2] }) // '1,00,00,000' (1 crore)
format('123456789', { locale: 'en-IN' }) // '12,34,56,789'
```

An explicit `group` always wins over the locale, and a single number stays
uniform grouping (`4` → `1234,5678` for 만-style). `group: 0` turns
separators off entirely.

### Korean amount reading (한글 금액 병기)

The "150만" hint that banking and fintech UIs render next to amount fields —
Expand Down Expand Up @@ -271,7 +291,7 @@ const [amount, setAmount] = createSignal('') // canonical
|---|---|---|
| `decimals` | `0` | max fraction digits (0 = integers only) |
| `negative` | `false` | allow a leading minus |
| `group` | `3` | digits per group (4 for 만-style grouping) |
| `group` | `3` | digits per group — `4` for 만-style, `[3, 2]` for Indian lakh, `0` for none |
| `separator` | `","` | group separator in the display |
| `decimalPoint` | `"."` | decimal mark in the display (canonical always uses `.`) |
| `locale` | — | **opt-in**: derive `separator`/`decimalPoint` via `Intl` — `"auto"` (browser language) or a BCP 47 tag. Without it the display is deterministic no matter the visitor's browser, which is what business forms usually need. Explicit `separator`/`decimalPoint` win. |
Expand Down Expand Up @@ -325,8 +345,10 @@ const [amount, setAmount] = createSignal('') // canonical
- ~~`v0.3` — Korean shorthand entry (`3만5천` → 35,000)~~ ✅ shipped
- ~~`v0.4` — smart deletion across separators + `min`/`max`~~ ✅ shipped
- ~~`v0.5` — Svelte action / Solid directive~~ ✅ shipped
- Indian lakh grouping (`12,34,567` — non-uniform group sizes) — demand-gated,
[open an issue](https://github.com/devslab-kr/numkey/issues) if you need it
- ~~`v0.6` — Indian lakh/crore grouping (`12,34,567`) + locale-derived group sizes~~ ✅ shipped

Nothing else is planned — [open an issue](https://github.com/devslab-kr/numkey/issues)
if your form needs something this doesn't do.

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@devslab/numkey",
"version": "0.5.0",
"version": "0.6.0",
"publishConfig": {
"access": "public"
},
Expand Down
1 change: 1 addition & 0 deletions site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ <h2>Locale (opt-in) <span class="ko">/ 로케일은 옵트인 — 기본값은
<option value="de-DE">de-DE — 1.234.567,89</option>
<option value="fr-FR">fr-FR — 1 234 567,89</option>
<option value="en-US">en-US — 1,234,567.89</option>
<option value="en-IN">en-IN — 12,34,567.89 (lakh)</option>
</select>
</label>
<input type="text" id="loc" data-numkey="2" value="1234567.89">
Expand Down
55 changes: 52 additions & 3 deletions src/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,37 @@ describe('format — canonical → display', () => {
).toBe('1.234.567,89')
})

it('groups the Indian way with [primary, secondary] sizes', () => {
const o = { group: [3, 2] as [number, number] }
expect(format('100000', o)).toBe('1,00,000') // 1 lakh
expect(format('10000000', o)).toBe('1,00,00,000') // 1 crore
expect(format('123456789', o)).toBe('12,34,56,789')
expect(format('1234', o)).toBe('1,234') // below the first cut
expect(format('999', o)).toBe('999')
})

it('matches Intl en-IN across integer widths', () => {
const nf = new Intl.NumberFormat('en-IN')
for (let width = 1; width <= 15; width++) {
const digits = '1234567890123456'.slice(0, width)
expect(format(digits, { group: [3, 2] })).toBe(nf.format(BigInt(digits)))
}
})

it('keeps negatives and decimals intact under Indian grouping', () => {
expect(
format('-12345678.9', { group: [3, 2], negative: true, decimals: 1 })
).toBe('-1,23,45,678.9')
})

it('treats a secondary size of 0 as uniform grouping', () => {
expect(format('123456789', { group: [3, 0] })).toBe('123,456,789')
})

it('turns grouping off with group 0', () => {
expect(format('1234567', { group: 0 })).toBe('1234567')
})

it('round-trips: format(parse(x)) is stable', () => {
for (const v of ['1,234,567', '007', '0.50', '-9,999']) {
const o = { decimals: 2, negative: true }
Expand All @@ -125,14 +156,31 @@ describe('locale — opt-in separator derivation via Intl', () => {
it('derives separators from a BCP 47 tag', () => {
expect(localeSeparators('de-DE')).toEqual({
separator: '.',
decimalPoint: ','
decimalPoint: ',',
group: [3, 3]
})
expect(localeSeparators('ko-KR')).toEqual({
separator: ',',
decimalPoint: '.'
decimalPoint: '.',
group: [3, 3]
})
})

it('derives the Indian group sizes from the tag', () => {
expect(localeSeparators('en-IN')).toEqual({
separator: ',',
decimalPoint: '.',
group: [3, 2]
})
expect(format('123456789', { locale: 'en-IN' })).toBe('12,34,56,789')
})

it('explicit group wins over the locale', () => {
expect(format('123456789', { locale: 'en-IN', group: 3 })).toBe(
'123,456,789'
)
})

it('format/parse round-trip under a locale', () => {
const o = { decimals: 2, locale: 'de-DE' }
expect(format('1234567.89', o)).toBe('1.234.567,89')
Expand All @@ -148,7 +196,8 @@ describe('locale — opt-in separator derivation via Intl', () => {
it('falls back to deterministic defaults on an invalid tag', () => {
expect(localeSeparators('no-such-locale-tag-!!!')).toEqual({
separator: ',',
decimalPoint: '.'
decimalPoint: '.',
group: [3, 3]
})
})

Expand Down
Loading