SuriDevs Logo
How I Built a Unit Converter with 54 Categories: Lessons from 3 Years of Development

How I Built a Unit Converter with 54 Categories: Lessons from 3 Years of Development

By Sagar Maiyad  Nov 01, 2025

Three years ago, I needed a unit converter for a client project. "How hard could it be?" I thought. Multiply by some conversion factor, done.

Yeah, that didn't age well.

That "simple" project turned into an app with 54 conversion categories, 800+ units, 18 financial calculators, and sensor-based tools like a compass and sound meter. I basically scope-creeped myself for three years straight.

The First Version Was Embarrassing

My initial approach to unit conversion:

fun convertLength(value: Double, from: String, to: String): Double {
    return when {
        from == "meter" && to == "kilometer" -> value / 1000
        from == "kilometer" && to == "meter" -> value * 1000
        from == "meter" && to == "mile" -> value * 0.000621371
        // ... 400 more combinations
        else -> value
    }
}

Works great for 5 units. Then I added more. And more. Suddenly I'm looking at 2,450 conversion cases for just 50 units. The complexity grows quadratically and I didn't think about that until I was already deep in it.

I rewrote the whole thing using what's called the base unit pattern. Each unit just knows how to convert to and from one base unit (meters for length, grams for weight, etc):

data class Unit(
    val id: String,
    val name: String,
    val toBase: (Double) -> Double,
    val fromBase: (Double) -> Double
)

fun convert(value: Double, from: Unit, to: Unit): Double {
    val baseValue = from.toBase(value)
    return to.fromBase(baseValue)
}

Now adding a unit is one line. Should've done this from day one.

Temperature Made Me Feel Stupid

I thought I had everything figured out. Then temperature broke my brain for an afternoon.

Celsius to Fahrenheit isn't just multiplication - there's an offset: (C × 9/5) + 32. I spent way too long trying to force it into my multiplication-only mental model before realizing my lambda approach already handled it:

Unit("fahrenheit", "Fahrenheit",
    toBase = { (it - 32) * 5 / 9 },    // to Celsius
    fromBase = { it * 9 / 5 + 32 }     // from Celsius
)

Lesson learned: what looks simple usually isn't. Design for weird cases early.

Currency Was a Whole Different Problem

Static conversions? Easy. Currency rates change every day. Sometimes multiple times a day.

I needed to fetch fresh rates from an API, cache them for offline use, update them in the background, and somehow tell users when the rates are stale. Here's the basic idea:

class CurrencyRepository(
    private val api: CurrencyApi,
    private val cache: CurrencyCache
) {
    suspend fun getRates(): List<Currency> {
        return try {
            val fresh = api.getLatestRates()
            cache.save(fresh)
            fresh
        } catch (e: Exception) {
            cache.get() ?: throw e
        }
    }
}

The cache fallback sounds obvious but I forgot it initially. Users on flights or in basements were getting crashes. Not great.

The HDFC Bank Incident

Users started requesting EMI and SIP calculators. Cool, I thought. Just plug in the formula:

EMI = P × r × (1 + r)^n / ((1 + r)^n - 1)

Implemented it. Tested it. Deployed it. Got emails saying my calculator was "wrong" because it didn't match HDFC Bank's website.

Turns out banks round EMI to the nearest rupee first, then recalculate everything based on that rounded number. Mathematically "correct" wasn't what users wanted - they wanted to match their bank statements.

fun calculateEMIBankStyle(principal: Double, rate: Double, months: Int): EMIResult {
    val rawEMI = calculateEMI(principal, rate, months)
    val roundedEMI = rawEMI.roundToLong().toDouble()

    val totalPayable = roundedEMI * months
    val totalInterest = totalPayable - principal

    return EMIResult(roundedEMI, totalInterest, totalPayable)
}

Oh, and 0% interest promotional loans? Division by zero. That was a fun crash report to wake up to.

"Can You Add a Compass?"

People kept asking for random utility tools. Compass, bubble level, sound meter. I figured why not, it's just sensors.

It's not "just sensors."

Building a compass needs both accelerometer (which way is down) and magnetometer (which way is north). They call this sensor fusion. Raw data from these sensors jumps around like crazy - you need filtering:

private fun lowPassFilter(input: FloatArray, output: FloatArray) {
    val alpha = 0.1f
    for (i in input.indices) {
        output[i] = output[i] + alpha * (input[i] - output[i])
    }
}

The bubble level was easier - just accelerometer. But then I learned sensors drain battery fast. Forgot to unregister listeners when app went to background. Users complained about battery drain. Fixed it.

Sound meter needed microphone permission, real-time audio processing with AudioRecord, converting amplitude to decibels... and then I had to figure out what decibel ranges actually mean. Is 50dB quiet? Loud? I ended up just labeling ranges like "Quiet" and "Loud" because raw numbers meant nothing to most users.

What Actually Worked

Looking back after three years:

The base-unit pattern saved me so much time. Every new unit is just one line now. I've added hundreds without touching conversion logic.

Matching bank calculators instead of being "mathematically correct" stopped the complaint emails overnight.

Testing on real phones caught issues emulators never showed. Sensor behavior varies a lot between devices.

What I'd Change

If I rebuilt this from scratch:

I'd use a category interface instead of sealed classes. Adding new category types is more annoying than it should be.

I'd separate display formatting from domain logic earlier. Right now it's messier than I'd like.

I'd write roundtrip tests for conversions from day one. Convert X to Y to X should give you X back. Found some precision bugs way too late.

Three Years Later

What started as a weekend project became something I actually use daily. It handles the boring conversions (length, weight) and the interesting stuff (currency, EMI, compass) without needing five different apps.

If you're building something similar, start with the base-unit pattern. Add complexity when users ask for it, not before. Most of what I learned came from shipping, getting complaints, and fixing things.

Android Kotlin Architecture Developer Story

Author

Sagar Maiyad
Written By
Sagar Maiyad

Sagar Maiyad - Android developer specializing in Kotlin, Jetpack Compose, and modern Android architecture. Sharing practical tutorials and real-world development insights.

View All Posts →

Latest Post

Latest Tags