• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

forzagreen / n2words / 26795379252

02 Jun 2026 02:52AM UTC coverage: 95.729% (-0.4%) from 96.117%
26795379252

push

github

web-flow
build: migrate to eslint 10 (drop neostandard for @eslint/js + @stylistic + compat) (#335)

5140 of 5605 branches covered (91.7%)

Branch coverage included in aggregate %.

1964 of 2178 new or added lines in 78 files covered. (90.17%)

4 existing lines in 2 files now uncovered.

30030 of 31134 relevant lines covered (96.45%)

26.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.39
/src/ru-RU.js
1
/**
1✔
2
 * Russian (Russia) language converter
1✔
3
 *
1✔
4
 * CLDR: ru-RU | Russian as used in Russia
1✔
5
 *
1✔
6
 * Key features:
1✔
7
 * - Three-form pluralization (one/few/many)
1✔
8
 * - Gender: thousands are feminine, millions+ are masculine
1✔
9
 * - Irregular hundreds (двести, триста, etc.)
1✔
10
 * - Long scale naming
1✔
11
 */
1✔
12

1✔
13
import { parseCardinalValue } from './utils/parse-cardinal.js'
1✔
14
import { parseCurrencyValue } from './utils/parse-currency.js'
1✔
15
import { parseOrdinalValue } from './utils/parse-ordinal.js'
1✔
16
import { validateOptions } from './utils/validate-options.js'
1✔
17

1✔
18
// ============================================================================
1✔
19
// Vocabulary
1✔
20
// ============================================================================
1✔
21

1✔
22
const ONES_MASC = ['', 'один', 'два', 'три', 'четыре', 'пять', 'шесть', 'семь', 'восемь', 'девять']
1✔
23
const ONES_FEM = ['', 'одна', 'две', 'три', 'четыре', 'пять', 'шесть', 'семь', 'восемь', 'девять']
1✔
24

1✔
25
const TEENS = ['десять', 'одиннадцать', 'двенадцать', 'тринадцать', 'четырнадцать', 'пятнадцать', 'шестнадцать', 'семнадцать', 'восемнадцать', 'девятнадцать']
1✔
26
const TENS = ['', '', 'двадцать', 'тридцать', 'сорок', 'пятьдесят', 'шестьдесят', 'семьдесят', 'восемьдесят', 'девяносто']
1✔
27

1✔
28
// Irregular hundreds
1✔
29
const HUNDREDS = ['', 'сто', 'двести', 'триста', 'четыреста', 'пятьсот', 'шестьсот', 'семьсот', 'восемьсот', 'девятьсот']
1✔
30

1✔
31
const ZERO = 'ноль'
1✔
32
const NEGATIVE = 'минус'
1✔
33
const DECIMAL_SEP = 'запятая'
1✔
34

1✔
35
// Scale words: [singular, few, many]
1✔
36
// Thousands (index 0) are feminine, rest are masculine
1✔
37
const SCALE_FORMS = [
1✔
38
  ['тысяча', 'тысячи', 'тысяч'],
1✔
39
  ['миллион', 'миллиона', 'миллионов'],
1✔
40
  ['миллиард', 'миллиарда', 'миллиардов'],
1✔
41
  ['триллион', 'триллиона', 'триллионов'],
1✔
42
  ['квадриллион', 'квадриллиона', 'квадриллионов'],
1✔
43
  ['квинтиллион', 'квинтиллиона', 'квинтиллионов'],
1✔
44
  ['секстиллион', 'секстиллиона', 'секстиллионов'],
1✔
45
  ['септиллион', 'септиллиона', 'септиллионов'],
1✔
46
  ['октиллион', 'октиллиона', 'октиллионов'],
1✔
47
  ['нониллион', 'нониллиона', 'нониллионов'],
1✔
48
]
1✔
49

1✔
50
// ============================================================================
1✔
51
// Ordinal Vocabulary (masculine nominative)
1✔
52
// ============================================================================
1✔
53

1✔
54
// Ordinal ones: первый, второй, третий...
1✔
55
const ORDINAL_ONES = ['', 'первый', 'второй', 'третий', 'четвёртый', 'пятый', 'шестой', 'седьмой', 'восьмой', 'девятый']
1✔
56

1✔
57
// Ordinal teens: десятый, одиннадцатый...
1✔
58
const ORDINAL_TEENS = ['десятый', 'одиннадцатый', 'двенадцатый', 'тринадцатый', 'четырнадцатый', 'пятнадцатый', 'шестнадцатый', 'семнадцатый', 'восемнадцатый', 'девятнадцатый']
1✔
59

1✔
60
// Ordinal tens: двадцатый, тридцатый...
1✔
61
const ORDINAL_TENS = ['', '', 'двадцатый', 'тридцатый', 'сороковой', 'пятидесятый', 'шестидесятый', 'семидесятый', 'восьмидесятый', 'девяностый']
1✔
62

1✔
63
// Ordinal hundreds: сотый, двухсотый...
1✔
64
const ORDINAL_HUNDREDS = ['', 'сотый', 'двухсотый', 'трёхсотый', 'четырёхсотый', 'пятисотый', 'шестисотый', 'семисотый', 'восьмисотый', 'девятисотый']
1✔
65

1✔
66
// Ordinal scale words (тысячный, миллионный, etc.)
1✔
67
const ORDINAL_SCALES = [
1✔
68
  'тысячный',
1✔
69
  'миллионный',
1✔
70
  'миллиардный',
1✔
71
  'триллионный',
1✔
72
  'квадриллионный',
1✔
73
  'квинтиллионный',
1✔
74
  'секстиллионный',
1✔
75
  'септиллионный',
1✔
76
  'октиллионный',
1✔
77
  'нониллионный',
1✔
78
]
1✔
79

1✔
80
// Prefixes for compound ordinal thousands (двух-, трёх-, etc. + тысячный)
1✔
81
const THOUSAND_PREFIXES = ['', '', 'двух', 'трёх', 'четырёх', 'пяти', 'шести', 'семи', 'восьми', 'девяти']
1✔
82

1✔
83
// ============================================================================
1✔
84
// Currency Vocabulary (Russian Ruble)
1✔
85
// ============================================================================
1✔
86

1✔
87
// Ruble: masculine, [singular, few, many]
1✔
88
const RUBLE_FORMS = ['рубль', 'рубля', 'рублей']
1✔
89

1✔
90
// Kopeck: feminine, [singular, few, many]
1✔
91
const KOPECK_FORMS = ['копейка', 'копейки', 'копеек']
1✔
92

1✔
93
// ============================================================================
1✔
94
// Segment Building
1✔
95
// ============================================================================
1✔
96

1✔
97
/**
1✔
98
 * Selects the correct plural form based on Russian pluralization rules.
1✔
99
 *
1✔
100
 * @param {number | bigint} n - The count
1✔
101
 * @param {string[]} forms - [one, few, many] forms
1✔
102
 * @returns {string} The matching plural form
1✔
103
 */
1✔
104
function pluralize(n, forms) {
97✔
105
  const num = typeof n === 'bigint' ? Number(n) : n
97✔
106
  const lastDigit = num % 10
97✔
107
  const lastTwoDigits = num % 100
97✔
108

97✔
109
  if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
97✔
110
    return forms[2]
4✔
111
  }
4✔
112

93✔
113
  if (lastDigit === 1) return forms[0]
97✔
114
  if (lastDigit >= 2 && lastDigit <= 4) return forms[1]
97✔
115
  return forms[2]
37✔
116
}
97✔
117

1✔
118
/**
1✔
119
 * Builds masculine cardinal words for a 0-999 segment.
1✔
120
 *
1✔
121
 * @param {number} n - Number 0-999
1✔
122
 * @returns {string} Masculine cardinal words
1✔
123
 */
1✔
124
function buildSegmentMasc(n) {
108✔
125
  if (n === 0) return ''
108!
126

108✔
127
  const onesDigit = n % 10
108✔
128
  const tensDigit = Math.trunc(n / 10) % 10
108✔
129
  const hundredsDigit = Math.trunc(n / 100)
108✔
130

108✔
131
  const parts = []
108✔
132

108✔
133
  if (hundredsDigit > 0) {
108✔
134
    parts.push(HUNDREDS[hundredsDigit])
32✔
135
  }
32✔
136

108✔
137
  if (tensDigit > 1) {
108✔
138
    parts.push(TENS[tensDigit])
53✔
139
  }
53✔
140

108✔
141
  if (tensDigit === 1) {
108✔
142
    parts.push(TEENS[onesDigit])
11✔
143
  }
11✔
144
  else if (onesDigit > 0) {
97✔
145
    parts.push(ONES_MASC[onesDigit])
82✔
146
  }
82✔
147

108✔
148
  return parts.join(' ')
108✔
149
}
108✔
150

1✔
151
/**
1✔
152
 * Builds feminine cardinal words for a 0-999 segment.
1✔
153
 *
1✔
154
 * @param {number} n - Number 0-999
1✔
155
 * @returns {string} Feminine cardinal words
1✔
156
 */
1✔
157
function buildSegmentFem(n) {
63✔
158
  if (n === 0) return ''
63!
159

63✔
160
  const onesDigit = n % 10
63✔
161
  const tensDigit = Math.trunc(n / 10) % 10
63✔
162
  const hundredsDigit = Math.trunc(n / 100)
63✔
163

63✔
164
  const parts = []
63✔
165

63✔
166
  if (hundredsDigit > 0) {
63✔
167
    parts.push(HUNDREDS[hundredsDigit])
2✔
168
  }
2✔
169

63✔
170
  if (tensDigit > 1) {
63✔
171
    parts.push(TENS[tensDigit])
16✔
172
  }
16✔
173

63✔
174
  if (tensDigit === 1) {
63✔
175
    parts.push(TEENS[onesDigit])
4✔
176
  }
4✔
177
  else if (onesDigit > 0) {
59✔
178
    parts.push(ONES_FEM[onesDigit])
52✔
179
  }
52✔
180

63✔
181
  return parts.join(' ')
63✔
182
}
63✔
183

1✔
184
// ============================================================================
1✔
185
// Conversion Functions
1✔
186
// ============================================================================
1✔
187

1✔
188
/**
1✔
189
 * Converts a non-negative integer to Russian cardinal words.
1✔
190
 *
1✔
191
 * @param {bigint} n - Non-negative integer to convert
1✔
192
 * @param {('masculine'|'feminine')} gender - Grammatical gender
1✔
193
 * @returns {string} Cardinal Russian words
1✔
194
 */
1✔
195
function integerToWords(n, gender) {
147✔
196
  if (n === 0n) return ZERO
147✔
197

144✔
198
  const feminine = gender === 'feminine'
144✔
199

144✔
200
  if (n < 1000n) {
147✔
201
    return feminine ? buildSegmentFem(Number(n)) : buildSegmentMasc(Number(n))
115✔
202
  }
115✔
203

29✔
204
  if (n < 1_000_000n) {
147✔
205
    const thousands = Number(n / 1000n)
20✔
206
    const remainder = Number(n % 1000n)
20✔
207

20✔
208
    // Thousands are always feminine in Russian
20✔
209
    const thousandsWord = buildSegmentFem(thousands)
20✔
210
    const scaleWord = pluralize(thousands, SCALE_FORMS[0])
20✔
211

20✔
212
    let result = thousandsWord + ' ' + scaleWord
20✔
213

20✔
214
    if (remainder > 0) {
20✔
215
      result += ' ' + (feminine ? buildSegmentFem(remainder) : buildSegmentMasc(remainder))
17✔
216
    }
17✔
217

20✔
218
    return result
20✔
219
  }
20✔
220

9✔
221
  return buildLargeNumberWords(n, gender)
9✔
222
}
147✔
223

1✔
224
/**
1✔
225
 * Builds cardinal words for numbers >= 1,000,000 via scale decomposition.
1✔
226
 *
1✔
227
 * @param {bigint} n - Number >= 1,000,000
1✔
228
 * @param {('masculine'|'feminine')} gender - Grammatical gender
1✔
229
 * @returns {string} Cardinal Russian words
1✔
230
 */
1✔
231
function buildLargeNumberWords(n, gender) {
9✔
232
  const feminine = gender === 'feminine'
9✔
233
  const numStr = n.toString()
9✔
234
  const len = numStr.length
9✔
235

9✔
236
  const segments = []
9✔
237
  const segmentSize = 3
9✔
238

9✔
239
  const remainderLen = len % segmentSize
9✔
240
  let pos = 0
9✔
241
  if (remainderLen > 0) {
9✔
242
    segments.push(Number(numStr.slice(0, remainderLen)))
8✔
243
    pos = remainderLen
8✔
244
  }
8✔
245
  while (pos < len) {
9✔
246
    segments.push(Number(numStr.slice(pos, pos + segmentSize)))
29✔
247
    pos += segmentSize
29✔
248
  }
29✔
249

9✔
250
  const parts = []
9✔
251
  let scaleIndex = segments.length - 1
9✔
252

9✔
253
  for (let i = 0; i < segments.length; i++) {
9✔
254
    const segment = segments[i]
37✔
255

37✔
256
    if (segment !== 0) {
37✔
257
      if (scaleIndex === 0) {
10✔
258
        parts.push(feminine ? buildSegmentFem(segment) : buildSegmentMasc(segment))
1!
259
      }
1✔
260
      else {
9✔
261
        const scaleForms = SCALE_FORMS[scaleIndex - 1]
9✔
262
        const scaleWord = pluralize(segment, scaleForms)
9✔
263
        // Thousands (scaleIndex=1) are feminine, others masculine
9✔
264
        const isFeminine = scaleIndex === 1
9✔
265
        const segmentWord = isFeminine ? buildSegmentFem(segment) : buildSegmentMasc(segment)
9!
266
        parts.push(segmentWord + ' ' + scaleWord)
9✔
267
      }
9✔
268
    }
10✔
269

37✔
270
    scaleIndex--
37✔
271
  }
37✔
272

9✔
273
  return parts.join(' ')
9✔
274
}
9✔
275

1✔
276
/**
1✔
277
 * Converts the fractional digit string to Russian words.
1✔
278
 *
1✔
279
 * @param {string} decimalPart - The fractional digits
1✔
280
 * @param {('masculine'|'feminine')} gender - Grammatical gender
1✔
281
 * @returns {string} Cardinal Russian words for the decimal part
1✔
282
 */
1✔
283
function decimalPartToWords(decimalPart, gender) {
9✔
284
  let result = ''
9✔
285
  let i = 0
9✔
286

9✔
287
  while (i < decimalPart.length && decimalPart[i] === '0') {
9✔
288
    if (result) result += ' '
3✔
289
    result += ZERO
3✔
290
    i++
3✔
291
  }
3✔
292

9✔
293
  const remainder = decimalPart.slice(i)
9✔
294
  if (remainder) {
9✔
295
    if (result) result += ' '
9✔
296
    result += integerToWords(BigInt(remainder), gender)
9✔
297
  }
9✔
298

9✔
299
  return result
9✔
300
}
9✔
301

1✔
302
/**
1✔
303
 * Converts a numeric value to Russian words.
1✔
304
 *
1✔
305
 * @param {number | string | bigint} value - The numeric value to convert
1✔
306
 * @param {Object} [options] - Optional configuration
1✔
307
 * @param {('masculine'|'feminine')} [options.gender='masculine'] - Grammatical gender
1✔
308
 * @returns {string} The number in Russian words
1✔
309
 */
1✔
310
function toCardinal(value, options) {
77✔
311
  options = validateOptions(options)
77✔
312
  const { isNegative, integerPart, decimalPart } = parseCardinalValue(value)
77✔
313

77✔
314
  // Apply option defaults
77✔
315
  const { gender = 'masculine' } = options
77✔
316

77✔
317
  let result = ''
77✔
318

77✔
319
  if (isNegative) {
77✔
320
    result = NEGATIVE + ' '
3✔
321
  }
3✔
322

77✔
323
  result += integerToWords(integerPart, gender)
77✔
324

77✔
325
  if (decimalPart) {
77✔
326
    result += ' ' + DECIMAL_SEP + ' ' + decimalPartToWords(decimalPart, gender)
9✔
327
  }
9✔
328

77✔
329
  return result
77✔
330
}
77✔
331

1✔
332
// ============================================================================
1✔
333
// ORDINAL: toOrdinal(value)
1✔
334
// ============================================================================
1✔
335

1✔
336
/**
1✔
337
 * Builds ordinal for a 0-99 segment when it's the final (ordinal) part.
1✔
338
 * Returns ordinal form: "первый", "двадцать первый", etc.
1✔
339
 *
1✔
340
 * @param {number} n - Number 0-99
1✔
341
 * @returns {string} Ordinal words
1✔
342
 */
1✔
343
function buildOrdinalTensOnes(n) {
48✔
344
  if (n === 0) return ''
48!
345

48✔
346
  const onesDigit = n % 10
48✔
347
  const tensDigit = Math.trunc(n / 10)
48✔
348

48✔
349
  if (tensDigit === 0) {
48✔
350
    // Single digit: первый, второй, etc.
13✔
351
    return ORDINAL_ONES[onesDigit]
13✔
352
  }
13✔
353

35✔
354
  if (tensDigit === 1) {
48✔
355
    // Teens: десятый, одиннадцатый, etc.
14✔
356
    return ORDINAL_TEENS[onesDigit]
14✔
357
  }
14✔
358

21✔
359
  // Tens >= 20
21✔
360
  if (onesDigit === 0) {
48✔
361
    // Round tens: двадцатый, тридцатый, etc.
8✔
362
    return ORDINAL_TENS[tensDigit]
8✔
363
  }
8✔
364

13✔
365
  // Compound: двадцать первый, тридцать второй, etc.
13✔
366
  return TENS[tensDigit] + ' ' + ORDINAL_ONES[onesDigit]
13✔
367
}
48✔
368

1✔
369
/**
1✔
370
 * Converts a positive integer to Russian ordinal words (masculine nominative).
1✔
371
 *
1✔
372
 * In Russian ordinals, only the LAST component becomes ordinal.
1✔
373
 * E.g., 121 = "сто двадцать первый" (one hundred twenty first)
1✔
374
 *
1✔
375
 * @param {bigint} n - Positive integer to convert
1✔
376
 * @returns {string} Ordinal Russian words
1✔
377
 */
1✔
378
function integerToOrdinal(n) {
75✔
379
  // Fast path: numbers < 100
75✔
380
  if (n < 100n) {
75✔
381
    return buildOrdinalTensOnes(Number(n))
39✔
382
  }
39✔
383

36✔
384
  // Fast path: numbers < 1000
36✔
385
  if (n < 1000n) {
75✔
386
    const num = Number(n)
19✔
387
    const hundredsDigit = Math.trunc(num / 100)
19✔
388
    const remainder = num % 100
19✔
389

19✔
390
    if (remainder === 0) {
19✔
391
      // Exact hundreds: сотый, двухсотый, etc.
10✔
392
      return ORDINAL_HUNDREDS[hundredsDigit]
10✔
393
    }
10✔
394

9✔
395
    // Has remainder: cardinal hundreds + ordinal remainder
9✔
396
    return HUNDREDS[hundredsDigit] + ' ' + buildOrdinalTensOnes(remainder)
9✔
397
  }
9✔
398

17✔
399
  // Fast path: numbers < 1,000,000
17✔
400
  if (n < 1_000_000n) {
75✔
401
    const thousands = Number(n / 1000n)
11✔
402
    const remainder = Number(n % 1000n)
11✔
403

11✔
404
    if (remainder === 0) {
11✔
405
      // Exact thousands: тысячный, двухтысячный, etc.
5✔
406
      if (thousands === 1) {
5✔
407
        return ORDINAL_SCALES[0] // тысячный
1✔
408
      }
1✔
409
      if (thousands < 10) {
5✔
410
        return THOUSAND_PREFIXES[thousands] + ORDINAL_SCALES[0]
3✔
411
      }
3✔
412
      // For larger thousands, use cardinal + тысячный
1✔
413
      return buildSegmentFem(thousands) + ' ' + ORDINAL_SCALES[0]
1✔
414
    }
1✔
415

6✔
416
    // Has remainder: cardinal thousands + ordinal remainder
6✔
417
    const thousandsWord = buildSegmentFem(thousands)
6✔
418
    const scaleWord = pluralize(thousands, SCALE_FORMS[0])
6✔
419
    return thousandsWord + ' ' + scaleWord + ' ' + integerToOrdinal(BigInt(remainder))
6✔
420
  }
6✔
421

6✔
422
  // For numbers >= 1,000,000, use scale decomposition
6✔
423
  return buildLargeOrdinal(n)
6✔
424
}
75✔
425

1✔
426
/**
1✔
427
 * Builds ordinal words for numbers >= 1,000,000.
1✔
428
 * All segments except the final one are cardinal; final segment is ordinal.
1✔
429
 *
1✔
430
 * @param {bigint} n - Number >= 1,000,000
1✔
431
 * @returns {string} Ordinal Russian words
1✔
432
 */
1✔
433
function buildLargeOrdinal(n) {
6✔
434
  const numStr = n.toString()
6✔
435
  const len = numStr.length
6✔
436

6✔
437
  // Extract segments (most-significant first)
6✔
438
  const segments = []
6✔
439
  const segmentSize = 3
6✔
440

6✔
441
  const remainderLen = len % segmentSize
6✔
442
  let pos = 0
6✔
443
  if (remainderLen > 0) {
6✔
444
    segments.push(Number(numStr.slice(0, remainderLen)))
6✔
445
    pos = remainderLen
6✔
446
  }
6✔
447
  while (pos < len) {
6✔
448
    segments.push(Number(numStr.slice(pos, pos + segmentSize)))
19✔
449
    pos += segmentSize
19✔
450
  }
19✔
451

6✔
452
  // Find the last non-zero segment
6✔
453
  let lastNonZeroIdx = segments.length - 1
6✔
454
  while (lastNonZeroIdx >= 0 && segments[lastNonZeroIdx] === 0) {
6✔
455
    lastNonZeroIdx--
17✔
456
  }
17✔
457

6✔
458
  const parts = []
6✔
459
  let scaleIndex = segments.length - 1
6✔
460

6✔
461
  for (let i = 0; i < segments.length; i++) {
6✔
462
    const segment = segments[i]
25✔
463

25✔
464
    if (segment !== 0) {
25✔
465
      const isLastNonZero = (i === lastNonZeroIdx)
7✔
466

7✔
467
      if (scaleIndex === 0) {
7✔
468
        // Units position (no scale)
1✔
469
        if (isLastNonZero) {
1✔
470
          parts.push(integerToOrdinal(BigInt(segment)))
1✔
471
        }
1✔
NEW
472
        else {
×
473
          parts.push(buildSegmentMasc(segment))
×
474
        }
×
475
      }
1✔
476
      else {
6✔
477
        // Has scale word
6✔
478
        if (isLastNonZero) {
6✔
479
          // This scale position is the final ordinal
5✔
480
          if (segment === 1) {
5✔
481
            parts.push(ORDINAL_SCALES[scaleIndex - 1])
4✔
482
          }
4✔
483
          else {
1✔
484
            // Use cardinal segment + ordinal scale
1✔
485
            const isFeminine = scaleIndex === 1 // thousands are feminine
1✔
486
            const segmentWord = isFeminine ? buildSegmentFem(segment) : buildSegmentMasc(segment)
1!
487
            parts.push(segmentWord + ' ' + ORDINAL_SCALES[scaleIndex - 1])
1✔
488
          }
1✔
489
        }
5✔
490
        else {
1✔
491
          // Not the final segment: use cardinal
1✔
492
          const scaleForms = SCALE_FORMS[scaleIndex - 1]
1✔
493
          const scaleWord = pluralize(segment, scaleForms)
1✔
494
          const isFeminine = scaleIndex === 1
1✔
495
          const segmentWord = isFeminine ? buildSegmentFem(segment) : buildSegmentMasc(segment)
1!
496
          parts.push(segmentWord + ' ' + scaleWord)
1✔
497
        }
1✔
498
      }
6✔
499
    }
7✔
500

25✔
501
    scaleIndex--
25✔
502
  }
25✔
503

6✔
504
  return parts.join(' ')
6✔
505
}
6✔
506

1✔
507
/**
1✔
508
 * Converts a numeric value to Russian ordinal words (masculine nominative).
1✔
509
 *
1✔
510
 * @param {number | string | bigint} value - The numeric value to convert (must be a positive integer)
1✔
511
 * @returns {string} The number as ordinal words (e.g., "первый", "сорок второй")
1✔
512
 * @throws {TypeError} If value is not a valid numeric type
1✔
513
 * @throws {RangeError} If value is negative, zero, or has a decimal part
1✔
514
 *
1✔
515
 * @example
1✔
516
 * toOrdinal(1)    // 'первый'
1✔
517
 * toOrdinal(2)    // 'второй'
1✔
518
 * toOrdinal(3)    // 'третий'
1✔
519
 * toOrdinal(21)   // 'двадцать первый'
1✔
520
 * toOrdinal(42)   // 'сорок второй'
1✔
521
 * toOrdinal(100)  // 'сотый'
1✔
522
 * toOrdinal(101)  // 'сто первый'
1✔
523
 * toOrdinal(1000) // 'тысячный'
1✔
524
 */
1✔
525
function toOrdinal(value) {
71✔
526
  const integerPart = parseOrdinalValue(value)
71✔
527
  return integerToOrdinal(integerPart)
71✔
528
}
71✔
529

1✔
530
// ============================================================================
1✔
531
// CURRENCY: toCurrency(value, options?)
1✔
532
// ============================================================================
1✔
533

1✔
534
/**
1✔
535
 * Converts a numeric value to Russian currency words (Russian Ruble).
1✔
536
 *
1✔
537
 * @param {number | string | bigint} value - The currency amount to convert
1✔
538
 * @param {Object} [options] - Optional configuration
1✔
539
 * @param {boolean} [options.and=true] - Use "и" between rubles and kopecks
1✔
540
 * @returns {string} The amount in Russian currency words
1✔
541
 * @throws {TypeError} If value is not a valid numeric type
1✔
542
 * @throws {Error} If value is not a valid number format
1✔
543
 *
1✔
544
 * @example
1✔
545
 * toCurrency(42.50)                    // 'сорок два рубля и пятьдесят копеек'
1✔
546
 * toCurrency(1)                        // 'один рубль'
1✔
547
 * toCurrency(0.99)                     // 'девяносто девять копеек'
1✔
548
 * toCurrency(0.01)                     // 'одна копейка'
1✔
549
 * toCurrency(42.50, { and: false })    // 'сорок два рубля пятьдесят копеек'
1✔
550
 */
1✔
551
function toCurrency(value, options) {
48✔
552
  options = validateOptions(options)
48✔
553
  const { isNegative, dollars: rubles, cents: kopecks } = parseCurrencyValue(value)
48✔
554
  const { and: useAnd = true } = options
48✔
555

48✔
556
  // Build result
48✔
557
  let result = ''
48✔
558
  if (isNegative) result = NEGATIVE + ' '
48✔
559

48✔
560
  // Rubles part (masculine) - show if non-zero, or if no kopecks
48✔
561
  if (rubles > 0n || kopecks === 0n) {
48✔
562
    result += integerToWords(rubles, 'masculine')
35✔
563
    result += ' ' + pluralize(rubles, RUBLE_FORMS)
35✔
564
  }
35✔
565

48✔
566
  // Kopecks part (feminine)
48✔
567
  if (kopecks > 0n) {
48✔
568
    if (rubles > 0n) {
26✔
569
      result += useAnd ? ' и ' : ' '
13✔
570
    }
13✔
571
    result += integerToWords(kopecks, 'feminine')
26✔
572
    result += ' ' + pluralize(kopecks, KOPECK_FORMS)
26✔
573
  }
26✔
574

48✔
575
  return result
48✔
576
}
48✔
577

1✔
578
// ============================================================================
1✔
579
// Exports
1✔
580
// ============================================================================
1✔
581

1✔
582
export { toCardinal, toOrdinal, toCurrency }
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc