Skip to content
Open
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
77 changes: 77 additions & 0 deletions app/api/routes-f/num-to-words/__tests__/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { convertNumberToWords } from '../_lib/converter';

describe('Number to Words Converter', () => {
describe('Cardinal Style', () => {
test('converts zero', () => {
expect(convertNumberToWords(0)).toBe('zero');
});

test('converts single digits', () => {
expect(convertNumberToWords(5)).toBe('five');
});

test('converts teens', () => {
expect(convertNumberToWords(13)).toBe('thirteen');
});

test('converts tens', () => {
expect(convertNumberToWords(20)).toBe('twenty');
expect(convertNumberToWords(21)).toBe('twenty-one');
});

test('converts hundreds', () => {
expect(convertNumberToWords(100)).toBe('one hundred');
expect(convertNumberToWords(123)).toBe('one hundred twenty-three');
});

test('converts large numbers', () => {
expect(convertNumberToWords(1000)).toBe('one thousand');
expect(convertNumberToWords(1000000)).toBe('one million');
expect(convertNumberToWords(1234567)).toBe('one million two hundred thirty-four thousand five hundred sixty-seven');
});

test('converts negative numbers', () => {
expect(convertNumberToWords(-1)).toBe('negative one');
expect(convertNumberToWords(-123)).toBe('negative one hundred twenty-three');
});

test('converts boundary value: 1 quadrillion', () => {
expect(convertNumberToWords(1000000000000000)).toBe('one quadrillion');
});

test('converts boundary value: -1 quadrillion', () => {
expect(convertNumberToWords(-1000000000000000)).toBe('negative one quadrillion');
});
});

describe('Ordinal Style', () => {
test('converts zero to zeroth', () => {
expect(convertNumberToWords(0, 'ordinal')).toBe('zeroth');
});

test('converts single digits', () => {
expect(convertNumberToWords(1, 'ordinal')).toBe('first');
expect(convertNumberToWords(2, 'ordinal')).toBe('second');
expect(convertNumberToWords(3, 'ordinal')).toBe('third');
expect(convertNumberToWords(4, 'ordinal')).toBe('fourth');
});

test('converts irregular ordinals', () => {
expect(convertNumberToWords(5, 'ordinal')).toBe('fifth');
expect(convertNumberToWords(8, 'ordinal')).toBe('eighth');
expect(convertNumberToWords(9, 'ordinal')).toBe('ninth');
expect(convertNumberToWords(12, 'ordinal')).toBe('twelfth');
});

test('converts compound ordinals', () => {
expect(convertNumberToWords(21, 'ordinal')).toBe('twenty-first');
expect(convertNumberToWords(100, 'ordinal')).toBe('one hundredth');
expect(convertNumberToWords(123, 'ordinal')).toBe('one hundred twenty-third');
});

test('converts large ordinals', () => {
expect(convertNumberToWords(1000, 'ordinal')).toBe('one thousandth');
expect(convertNumberToWords(1000000, 'ordinal')).toBe('one millionth');
});
});
});
123 changes: 123 additions & 0 deletions app/api/routes-f/num-to-words/_lib/converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { NumberStyle } from './types';

const ONES = [
'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine',
'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'
];

const TENS = [
'', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'
];

const SCALES = [
'', 'thousand', 'million', 'billion', 'trillion', 'quadrillion'
];

/**
* Converts a number to its cardinal English word representation.
* Range: -1 quadrillion to 1 quadrillion.
*/
export function toCardinal(n: number): string {
if (n === 0) return ONES[0];

Check failure on line 21 in app/api/routes-f/num-to-words/_lib/converter.ts

View workflow job for this annotation

GitHub Actions / quality

Expected { after 'if' condition

if (n < 0) {
return `negative ${toCardinal(Math.abs(n))}`;
}

const parts: string[] = [];
let scaleIndex = 0;
let remaining = Math.abs(n);

while (remaining > 0) {
const chunk = remaining % 1000;
if (chunk > 0) {
const chunkWords = convertChunk(chunk);
const scale = SCALES[scaleIndex];
parts.unshift(scale ? `${chunkWords} ${scale}` : chunkWords);
}
remaining = Math.floor(remaining / 1000);
scaleIndex++;
}

return parts.join(' ').trim();
}

/**
* Converts a 3-digit chunk to words.
*/
function convertChunk(n: number): string {
const words: string[] = [];
const hundreds = Math.floor(n / 100);
const remainder = n % 100;

if (hundreds > 0) {
words.push(`${ONES[hundreds]} hundred`);
}

if (remainder > 0) {
if (remainder < 20) {
words.push(ONES[remainder]);
} else {
const tens = Math.floor(remainder / 10);
const ones = remainder % 10;
words.push(ones > 0 ? `${TENS[tens]}-${ONES[ones]}` : TENS[tens]);
}
}

return words.join(' ');
}

/**
* Converts a number to its ordinal English word representation.
*/
export function toOrdinal(n: number): string {
const cardinal = toCardinal(n);

// Rule: Only the last word of the cardinal representation is transformed.
const words = cardinal.split(' ');
const lastWord = words.pop()!;

let ordinalLastWord = '';

// Special cases for ordinals
const ordinalMap: Record<string, string> = {
'one': 'first',
'two': 'second',
'three': 'third',
'five': 'fifth',
'eight': 'eighth',
'nine': 'ninth',
'twelve': 'twelfth',
'zero': 'zeroth'
};

// Handle hyphenated numbers (e.g., twenty-one -> twenty-first)
if (lastWord.includes('-')) {
const [tens, ones] = lastWord.split('-');
if (ordinalMap[ones]) {
ordinalLastWord = `${tens}-${ordinalMap[ones]}`;
} else {
ordinalLastWord = `${tens}-${ones}th`;
}
} else if (ordinalMap[lastWord]) {
ordinalLastWord = ordinalMap[lastWord];
} else if (lastWord.endsWith('y')) {
// twenty -> twentieth
ordinalLastWord = lastWord.slice(0, -1) + 'ieth';
} else {
ordinalLastWord = lastWord + 'th';
}

words.push(ordinalLastWord);
return words.join(' ');
}

/**
* Main entry point for conversion.
*/
export function convertNumberToWords(n: number, style: NumberStyle = 'short'): string {
if (style === 'ordinal') {
return toOrdinal(n);
}
return toCardinal(n);
}
10 changes: 10 additions & 0 deletions app/api/routes-f/num-to-words/_lib/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type NumberStyle = 'short' | 'ordinal';

export interface ConverterOptions {
style?: NumberStyle;
}

export interface ApiResponse {
words: string;
error?: string;
}
45 changes: 45 additions & 0 deletions app/api/routes-f/num-to-words/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { NextRequest, NextResponse } from 'next/server';
import { convertNumberToWords } from './_lib/converter';
import { NumberStyle, ApiResponse } from './_lib/types';

const MAX_LIMIT = 1_000_000_000_000_000; // 1 quadrillion
const MIN_LIMIT = -1_000_000_000_000_000;

export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const nStr = searchParams.get('n');
const style = (searchParams.get('style') || 'short') as NumberStyle;

if (nStr === null) {
return NextResponse.json(
{ error: "Query parameter 'n' is required" } as ApiResponse,
{ status: 400 }
);
}

const n = parseInt(nStr, 10);

if (isNaN(n)) {
return NextResponse.json(
{ error: "Query parameter 'n' must be a valid integer" } as ApiResponse,
{ status: 400 }
);
}

if (n > MAX_LIMIT || n < MIN_LIMIT) {
return NextResponse.json(
{ error: `Number out of range. Supported range: ${MIN_LIMIT} to ${MAX_LIMIT}` } as ApiResponse,
{ status: 400 }
);
}

try {
const words = convertNumberToWords(n, style);
return NextResponse.json({ words } as ApiResponse);
} catch (err) {

Check failure on line 39 in app/api/routes-f/num-to-words/route.ts

View workflow job for this annotation

GitHub Actions / quality

'err' is defined but never used
return NextResponse.json(
{ error: "Internal server error during conversion" } as ApiResponse,
{ status: 500 }
);
}
}
Loading