INTERMEDIATEJAVAUTILITIES

Convert Numbers to Words in Java (Indian Numbering System)

Java utility that converts amounts to words using the Indian numbering system (Crore, Lakh, Thousand). Handles BigDecimal, decimals, and edge cases.

By Tested on Java 21
Published Aug 21, 2024Updated May 25, 2026

NumberToWordsConverter turns a numeric amount into a word string using the Indian numbering system (Crore, Lakh, Thousand). It works on any Number (Integer, Long, Double, BigDecimal), rounds to 2 decimal places, and emits the standard "Rupees X Point Y Z" shape used on Indian invoices, cheques, and statutory documents.

Tested on Java 21.

When to Use This

  • Generating amount-in-words on invoices, receipts, or PDFs
  • Rendering legal documents that require the Indian numbering format
  • Converting fintech transaction amounts for SMS or email confirmations
  • Audit logs where humans cross-check numeric and verbal totals

Don't use this when you need the short-scale "million / billion" format. This converter is intentionally Indian-only. For locale-aware formatting, use ICU4J or the RuleBasedNumberFormat from java.text with a custom rule set.

Code

import java.math.BigDecimal;
import java.math.RoundingMode;
 
public class NumberToWordsConverter {
 
    private static final String[] units = {
        "", "One", "Two", "Three", "Four", "Five", "Six",
        "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve",
        "Thirteen", "Fourteen", "Fifteen", "Sixteen",
        "Seventeen", "Eighteen", "Nineteen"
    };
 
    private static final String[] tens = {
        "", "", "Twenty", "Thirty", "Forty",
        "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"
    };
 
    public static String convertNumberToWords(Number amount) {
        BigDecimal bdAmount = new BigDecimal(amount.toString())
            .setScale(2, RoundingMode.HALF_UP);
 
        if (bdAmount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Amount must be positive or zero.");
        }
        BigDecimal maxAmount = new BigDecimal("1000000000"); // 100 crore
        if (bdAmount.compareTo(maxAmount) >= 0) {
            throw new IllegalArgumentException("Amount must be less than 100 crore.");
        }
 
        BigDecimal integerPart = bdAmount.setScale(0, RoundingMode.DOWN);
        BigDecimal fractionalPart = bdAmount.subtract(integerPart)
            .multiply(new BigDecimal(100))
            .setScale(0, RoundingMode.HALF_UP);
 
        StringBuilder result = new StringBuilder();
        if (integerPart.compareTo(BigDecimal.ZERO) > 0) {
            result.append(convertIntegerToWords(integerPart.toBigInteger().longValue()));
        } else {
            result.append("Zero");
        }
 
        if (fractionalPart.compareTo(BigDecimal.ZERO) > 0) {
            result.append(" Point");
            for (char digit : fractionalPart.toPlainString().toCharArray()) {
                result.append(" ").append(units[Character.getNumericValue(digit)]);
            }
        }
 
        return result.toString().trim();
    }
 
    private static String convertIntegerToWords(long number) {
        if (number == 0) return "";
 
        StringBuilder word = new StringBuilder();
 
        if ((number / 10000000) > 0) {
            word.append(convertIntegerToWords(number / 10000000)).append(" Crore ");
            number %= 10000000;
        }
        if ((number / 100000) > 0) {
            word.append(convertIntegerToWords(number / 100000)).append(" Lakh ");
            number %= 100000;
        }
        if ((number / 1000) > 0) {
            word.append(convertIntegerToWords(number / 1000)).append(" Thousand ");
            number %= 1000;
        }
        if ((number / 100) > 0) {
            word.append(convertIntegerToWords(number / 100)).append(" Hundred ");
            number %= 100;
        }
        if (number > 0) {
            if (word.length() > 0) word.append("and ");
            if (number < 20) {
                word.append(units[(int) number]).append(" ");
            } else {
                word.append(tens[(int) (number / 10)])
                    .append(" ")
                    .append(units[(int) (number % 10)])
                    .append(" ");
            }
        }
        return word.toString().trim();
    }
}

The integer half uses recursion: each Indian tier (Crore, Lakh, Thousand, Hundred) chops off its order of magnitude and recurses on the remainder. Decimal handling is digit-by-digit, which is what cheques and invoices expect for the "Paise" component.

Usage

public class Main {
    public static void main(String[] args) {
        System.out.println(NumberToWordsConverter.convertNumberToWords(123));
        // One Hundred and Twenty Three
 
        System.out.println(NumberToWordsConverter.convertNumberToWords(12345678));
        // One Crore Twenty Three Lakh Forty Five Thousand Six Hundred and Seventy Eight
 
        System.out.println(NumberToWordsConverter.convertNumberToWords(123.45));
        // One Hundred and Twenty Three Point Four Five
 
        System.out.println(NumberToWordsConverter.convertNumberToWords(new BigDecimal("99999.99")));
        // Ninety Nine Thousand Nine Hundred and Ninety Nine Point Nine Nine
 
        System.out.println(NumberToWordsConverter.convertNumberToWords(0));
        // Zero
    }
}

Pass any Number. The converter normalises through BigDecimal#toString() so Double, Float, and raw BigDecimal all behave the same.

Pitfalls

  • Double precision creeps in. 0.1 + 0.2 becomes 0.30000000000000004 and rounds to Point Three Zero. For money use BigDecimal directly, not double.
  • Locale on toLowerCase is missing in some forks. If you customise the output, always pass Locale.ROOT to string case methods, otherwise Turkish locale will turn I into ı and break output.
  • No currency word. The output is the number only. If you need "Rupees" / "Paise" prefixes, wrap the call: "Rupees " + convertNumberToWords(amount) + " Only".
  • Recursion depth is fine for 100 crore. No StackOverflowError risk at this range, but if you extend to Arab / Kharab tiers, mind the recursion.
  • Hardcoded 2-decimal scale. Amounts like 0.005 round to 0.01. If your domain uses 4 decimal places, adjust setScale.

Frequently Asked Questions

Why use Indian numbering instead of the standard short scale?

Indian numbering (Lakh = 100,000 and Crore = 10,000,000) is the official format used on cheques, invoices, and statutory filings across India. Most fintech and accounting apps targeting India need this exact word format on PDFs, receipts, and amount-in-words fields, which the short-scale 'million / billion' format does not match.

Does this support negative numbers or amounts above 100 crore?

Negative numbers throw `IllegalArgumentException` by design (cheque amounts are non-negative). Amounts at or above 100 crore (1,000,000,000) throw as well so the recursion stays bounded. If you need 100 crore plus, extend `convertIntegerToWords` with an `Arab` (10 crore) or `Kharab` (100 crore) tier.

X (Twitter)LinkedIn