Wes Matlock

Flying High with Swift’s NumberFormatter: Tips, Tricks & Edge Cases

You ever find yourself formatting numbers, thinking it’s going to be a smooth ride… and suddenly your loyalty points show up without…


Flying High with Swift’s NumberFormatter: Tips, Tricks & Edge Cases

You ever find yourself formatting numbers, thinking it’s going to be a smooth ride… and suddenly your loyalty points show up without commas, or your ticket price is rounded weirdly because some locale flipped the script? Been there. Fixed that.

Working at Frontier Airlines, numbers are everywhere — ticket prices, seat numbers, frequent flyer miles, baggage weight. And trust me, one misplaced NumberFormatter config can throw the whole app into turbulence.

Today, we’re taking NumberFormatter all the way up to cruising altitude: basic formats, locales, rounding, grouping, spelled-out numbers, even an interview brain-buster at the end. Oh, and fun fact — I’ve literally flown cross-country just to catch a Metallica show. So yeah, I’ve got a thing for tickets and numbers.


✈️ 1. Ticket Price Formatting (Currency)

Classic. Customers want to know how much they’re paying, formatted cleanly.

import SwiftUI  
  
struct TicketPriceView: View {  
    let price: Double  
  
    var formattedPrice: String {  
        let formatter = NumberFormatter()  
        formatter.numberStyle = .currency  
        formatter.locale = Locale(identifier: "en_US") // USD  
        return formatter.string(from: NSNumber(value: price)) ?? "$0.00"  
    }  
  
    var body: some View {  
        VStack(spacing: 10) {  
            Text("Ticket Price")  
            Text(formattedPrice)  
                .font(.largeTitle)  
        }  
        .padding()  
    }  
}

🗝️ Key Points:

  • .currency automatically adds $ and two decimal places based on locale.
  • Locale flexibility: Switch “en_US” to “fr_FR” for Euros or “ja_JP” for Yen.
  • Always safely unwrap with ?? “$0.00” fallback to avoid crashes.

✅ Swift Test:

import Testing  
import Foundation  
  
@Suite  
struct TicketPriceTests {  
      
    @Test  
    func testTicketPriceFormatting() {  
        let formatter = NumberFormatter()  
        formatter.numberStyle = .currency  
        formatter.locale = Locale(identifier: "en_US")  
        let formatted = formatter.string(from: NSNumber(value: 249.99))  
        #expect(formatted == "$249.99")  
    }  
}


🎯 2. Loyalty Points (Decimal with Grouping)

Ever seen a customer with 1500000 points and no commas? Nightmare.

struct LoyaltyPointsView: View {  
    let points: Int  
  
    var formattedPoints: String {  
        let formatter = NumberFormatter()  
        formatter.numberStyle = .decimal  
        formatter.groupingSeparator = ","  
        formatter.groupingSize = 3  
        return formatter.string(from: NSNumber(value: points)) ?? "0"  
    }  
  
    var body: some View {  
        VStack(spacing: 10) {  
            Text("Loyalty Points")  
            Text(formattedPoints)  
                .font(.title)  
        }  
        .padding()  
    }  
}

🗝️ Key Points:

  • .decimal adds grouping separators but no symbols.
  • Explicitly setting groupingSeparator ensures control, regardless of locale.
  • Great for large numbers: frequent flyer miles, loyalty balances, etc.

✅ Swift Test:

@Suite  
struct LoyaltyPointsTests {  
      
    @Test  
    func testLoyaltyPointsFormatting() {  
        let formatter = NumberFormatter()  
        formatter.numberStyle = .decimal  
        formatter.groupingSeparator = ","  
        formatter.groupingSize = 3  
        let formatted = formatter.string(from: NSNumber(value: 1500000))  
        #expect(formatted == "1,500,000")  
    }  
}


📈 3. Baggage Weight (Measurement + Scientific)

Sometimes baggage weight tips over, and for fun (or a nerdy UI feature), you want to show it in scientific notation.

struct BaggageWeightView: View {  
    let weight: Double // in kilograms  
  
    var formattedWeight: String {  
        let formatter = NumberFormatter()  
        formatter.numberStyle = .scientific  
        formatter.positiveFormat = "0.###E+0 kg"  
        return formatter.string(from: NSNumber(value: weight)) ?? "N/A"  
    }  
  
    var body: some View {  
        VStack(spacing: 10) {  
            Text("Baggage Weight")  
            Text(formattedWeight)  
                .font(.title)  
        }  
        .padding()  
    }  
}

🗝️ Key Points:

  • .scientific helps when displaying large/small numbers compactly.
  • Customize with positiveFormat (kg added for clarity).
  • Less common but shows formatter flexibility.

✅ Swift Test:

@Suite  
struct BaggageWeightTests {  
      
    @Test  
    func testBaggageWeightFormatting() {  
        let formatter = NumberFormatter()  
        formatter.numberStyle = .scientific  
        formatter.positiveFormat = "0.###E+0 kg"  
        let formatted = formatter.string(from: NSNumber(value: 250.0))  
        #expect(formatted == "2.5E+2 kg")  
    }  
}


🪑 4. Seat Number (Ordinal)

Yes, you can make seat numbers human-readable!

struct SeatNumberView: View {  
    let seat: Int  
  
    var formattedSeat: String {  
        let formatter = NumberFormatter()  
        formatter.numberStyle = .ordinal  
        return formatter.string(from: NSNumber(value: seat)) ?? "\(seat)"  
    }  
  
    var body: some View {  
        VStack(spacing: 10) {  
            Text("Seat Number")  
            Text(formattedSeat)  
                .font(.title)  
        }  
        .padding()  
    }  
}

🗝️ Key Points:

  • .ordinal turns 12 into “12th”.
  • Locale-aware (differs in other languages).
  • Cleaner UX when displaying row/seat info.

✅ Swift Test:

@Suite  
struct SeatNumberTests {  
      
    @Test  
    func testSeatNumberFormatting() {  
        let formatter = NumberFormatter()  
        formatter.numberStyle = .ordinal  
        let formatted = formatter.string(from: NSNumber(value: 12))  
        #expect(formatted == "12th")  
    }  
}


🌎 5. Locale-Specific Pricing (Euro Example)

Maybe you’re booking a flight to Paris for a Metallica gig. Let’s show Euros:

struct EuroPriceView: View {  
    let price: Double  
  
    var formattedPrice: String {  
        let formatter = NumberFormatter()  
        formatter.numberStyle = .currency  
        formatter.locale = Locale(identifier: "fr_FR")  
        return formatter.string(from: NSNumber(value: price)) ?? "€0,00"  
    }  
  
    var body: some View {  
        VStack(spacing: 10) {  
            Text("EU Ticket Price")  
            Text(formattedPrice)  
                .font(.largeTitle)  
        }  
        .padding()  
    }  
}

🗝️ Key Points:

  • Locale changes decimal & grouping separators (299,99 € in France).
  • Avoids currency mismatches in international apps.
  • Easy to swap locales dynamically at runtime.

✅ Swift Test:

@Suite  
struct EuroPriceTests {  
      
    @Test  
    func testEuroPriceFormatting() {  
        let formatter = NumberFormatter()  
        formatter.numberStyle = .currency  
        formatter.locale = Locale(identifier: "fr_FR")  
        let formatted = formatter.string(from: NSNumber(value: 299.99))  
        #expect(formatted == "299,99 €")  
    }  
}


📜 6. Spell Out: Loyalty Points (Fun & Fancy)

Sometimes you just wanna spell out the points. Fancy receipts? Sure.

struct SpelledOutPointsView: View {  
    let points: Int  
  
    var formattedPoints: String {  
        let formatter = NumberFormatter()  
        formatter.numberStyle = .spellOut  
        return formatter.string(from: NSNumber(value: points)) ?? ""  
    }  
  
    var body: some View {  
        VStack(spacing: 10) {  
            Text("Points (Spelled Out)")  
            Text(formattedPoints)  
                .font(.title3)  
        }  
        .padding()  
    }  
}

🗝️ Key Points:

  • .spellOut = human-readable text version of numbers.
  • Can get very long with large numbers (you’ll see why in a sec).
  • Useful for stylized receipts or playful UI.

✅ Swift Test:

  
@Suite  
struct SpelledOutPointsViewTests {  
      
    @Test  
    func testSpelledOutPointsFormatting() {  
        let points = 1500  
        let formatter = NumberFormatter()  
        formatter.numberStyle = .spellOut  
        let expected = formatter.string(from: NSNumber(value: points))  
          
        #expect(expected == "one thousand five hundred")  
    }  
      
    @Test  
    func testSpelledOutPointsZero() {  
        let points = 0  
        let formatter = NumberFormatter()  
        formatter.numberStyle = .spellOut  
        let expected = formatter.string(from: NSNumber(value: points))  
          
        #expect(expected == "zero")  
    }  
}


🎤 Interview Challenge: Longest Spelled-Out Number (0–2300)

This one shows up in interviews. Find the number with the longest spelled-out name between 0 and 2300.

Here’s a brute-force but effective way:

func longestSpelledOutNumber(upTo limit: Int) -> (number: Int, word: String) {  
    let formatter = NumberFormatter()  
    formatter.numberStyle = .spellOut  
      
    var longest = (number: 0, word: "")  
      
    for i in 0...limit {  
        if let word = formatter.string(from: NSNumber(value: i)) {  
            if word.count > longest.word.count {  
                longest = (i, word)  
            }  
        }  
    }  
      
    return longest  
}  
  
struct LongestSpelledOutNumberView: View {  
    let limit: Int  
  
    var result: (number: Int, word: String) {  
        longestSpelledOutNumber(upTo: limit)  
    }  
  
    var body: some View {  
        VStack(spacing: 16) {  
            Text("Longest Spelled-Out Number")  
                .font(.headline)  
  
            Text("\(result.number)")  
                .font(.largeTitle)  
                .bold()  
                .padding(.bottom, 8)  
  
            Text(result.word.capitalized)  
                .multilineTextAlignment(.center)  
                .padding(.horizontal)  
  
            Text("(\(result.word.count) characters)")  
                .font(.caption)  
                .foregroundColor(.gray)  
        }  
        .padding()  
    }  
}

🗝️ Key Points:

  • Brute force approach: loops through all numbers up to limit.
  • Uses .spellOut for each.
  • Compares character counts to track the longest.

✅ Swift Tests (Including Performance!)

@Suite  
struct LongestSpelledOutNumberTests {  
  
    @Test  
    func testLongestNumberUpTo2300() {  
        let result = longestSpelledOutNumber(upTo: 2300)  
        #expect(result.number == 2222)  
        #expect(result.word.contains("two thousand two hundred twenty-two"))  
        #expect(result.word.count > 30)  
    }  
      
    @Test  
    func performanceTestUpTo2300() {  
        measure {  
            _ = longestSpelledOutNumber(upTo: 2300)  
        }  
    }  
}

✨ Key Takeaways:

  • The test confirms correctness & ensures locale behavior is stable.
  • Performance test: Swift Testing’s measure block tracks execution time.
  • Typically runs in milliseconds — shows it’s efficient even for 2,300 iterations.
  • Real interview flex material!

✈️ Wrapping It Up:

We covered:

  • Currency (USD & Euro)
  • Decimal with grouping (loyalty points)
  • Scientific (baggage weight)
  • Ordinal (seat numbers)
  • Spell out (fun!)
  • Locale quirks
  • And a killer interview challenge

Lesson? NumberFormatter can be your co-pilot or completely crash your app if misused. Know its quirks, test it thoroughly, and never assume the defaults are “good enough.”

If you’re hungry for more tips, tricks, and a few battle-tested stories from the trenches of native mobile development, swing by my collection of articles: https://medium.com/@wesleymatlock.

These posts are packed with real-world solutions, some laughs, and the kind of knowledge that’s saved me from a few late-night debugging sessions. Let’s keep building apps that rock — and if you’ve got questions or stories of your own, drop me a line. I’d love to hear from you.

Now if you’ll excuse me… I’ve got a flight to catch — Metallica doesn’t play itself. 🤘

By Wesley Matlock on March 24, 2025.

Canonical link

Exported from Medium on May 10, 2025.

Written on March 24, 2025