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.
Exported from Medium on May 10, 2025.