In my app I've got a certain distance in meters.
And I want to display it in kilometers if user prefers kilometers and display it in miles if user prefers miles. And in the first case I want to add to a string "kilometers" at the end and in the second one to add "miles".
What is the best way to achieve this goal?
Thanks.
To determine whether the user uses metric or not, NSLocale can tell you:
- (BOOL)isMetric {
return [[[NSLocale currentLocale] objectForKey:NSLocaleUsesMetricSystem] boolValue];
}
Swift equivalent of Chris' answer would be something like this:
func isMetric() -> Bool {
return ((Locale.current as NSLocale).object(forKey: NSLocale.Key.usesMetricSystem) as? Bool) ?? true
}
Note that it defaults to true under certain circumstances. Change as needed.
You could ask the user whether they prefer miles or kilometers, in a preference or something. Then whenever you display a distance you would say.
In pseudo c code
function distance(meters) {
if (userPrefersKM) {
return meters / 1000 + " kilometers";
else if (userPrefersMiles) {
return meters / METERS_IN_A_MILE + " miles";
}
Where METERS_IN_A_MILE would be about 1600, but you should look that up.
In Swift, Locale.current.usesMetricSystem gives what the user would expect. But you don't need that if you use Measurement which handles it for you.
let distanceInMeters: Double = 2353.45
let formatter = MeasurementFormatter()
formatter.unitStyle = .medium // adjust according to your need
let distance = Measurement(value: distanceInMeters, unit: UnitLength.meters)
formatter.string(from: distance)
The current locale dictates how it is presented to the user. To see how it works for different locales, try this in a Xcode Playground (examples are for UK and France):
let distanceInMeters: Double = 2353.45
let formatter = MeasurementFormatter()
formatter.unitStyle = .medium // adjust according to your need
let distance = Measurement(value: distanceInMeters, unit: UnitLength.meters)
formatter.locale = Locale(identifier: "en_UK")
formatter.string(from: distance) // 1.462 mi
formatter.locale = Locale(identifier: "en_FR")
formatter.string(from: distance) // 2,353 km
Unless the iPhone provides this information directly, you'll have to have a lookup table from locale to default unit. Then you should allow the user to override that default.
Related
I have an precision issue when dealing with currency input using Decimal type. The issue is with the formatter. This is the minimum reproducible code in playground:
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.isLenient = true
formatter.maximumFractionDigits = 2
formatter.generatesDecimalNumbers = true
let text = "89806.9"
let decimal = formatter.number(from: text)?.decimalValue ?? .zero
let string = "\(decimal)"
print(string)
It prints out 89806.89999999999 instead of 89806.9. However, most other numbers are fine (e.g. 8980.9). So I don't think this is a Double vs Decimal problem.
Edit:
The reason I need to use the formatter is that sometimes I need to deal with currency format input:
let text = "$89,806.9"
let decimal = formatter.number(from: text)?.decimalValue ?? .zero
print("\(decimal)") // prints 89806.89999999999
let text2 = "$89,806.9"
let decimal2 = Decimal(string: text2)
print("\(decimal2)") // prints nil
Using the new FormatStyle seems to generate the correct result
let format = Decimal.FormatStyle
.number
.precision(.fractionLength(0...2))
let text = "89806.9"
let value = try! format.parseStrategy.parse(text)
Below is an example parsing a currency using the currency code from the locale
let currencyFormat = Decimal.FormatStyle.Currency
.currency(code: Locale.current.currencyCode!)
.precision(.fractionLength(0...2))
let amount = try! currencyFormat.parseStrategy.parse(text)
Swedish example:
let text = "89806,9 kr"
print(amount)
89806.9
Another option is to use the new init for Decimal that takes a String and a FormatStyle.Currency (or a Number or Percent)
let amount = try Decimal(text, format: currencyFormat)
and to format this value we can use formatted(_:) on Decimal
print(amount.formatted(currencyFormat))
Output (still Swedish):
89 806,9 kr
I agree that this is a surprising bug, and I would open an Apple Feedback about it, but I would also highly recommend switching to Decimal(string:locale:) rather than a formatter, which will achieve your goal (except perhaps the isLenient part).
let x = Decimal(string: text)!
print("\(x)") // 89806.9
If you want to fix fraction digits, you can apply rounding pretty easily with * 100 / 100 conversions through Int. (I'll explain if it's not obvious how to do this; it works for Decimal, though not Double.)
Following Joakim Danielson Answer see this amazing documentation on the format style
Decimal(10.01).formatted(.number.precision(.fractionLength(1))) // 10.0 Decimal(10.01).formatted(.number.precision(.fractionLength(2))) // 10.01 Decimal(10.01).formatted(.number.precision(.fractionLength(3))) // 10.010
Amazingly detailed documentation
If this is strictly a rendering issue and you're just looking to translate a currency value from raw string to formatted string then just do that.
let formatter = NumberFormatter()
formatter.numberStyle = .currency
let raw = "89806.9"
if let double = Double(raw),
let currency = formatter.string(from: NSNumber(value: double)) {
print(currency) // $89,806.90
}
If there is math involved then before you get to the use of string formatters, I would point you to
Why not use Double or Float to represent currency? and
How to round a double to an int using Banker's Rounding in C as great starting points.
I get my response with double value and remove formatter.generatesDecimalNumbers line to get work.
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.isLenient = true
formatter.maximumFractionDigits = 2
//formatter.generatesDecimalNumbers = true // I removed this line
let text = "$89806.9"
let double = formatter.number(from: text)?.doubleValue ?? .zero // converting as double or float
let string = "\(double)"
print(string) // 89806.9
let anotherText = "$0.1"
let anotherDouble = formatter.number(from: anotherText)?.doubleValue ?? .zero // converting as double or float
let anotherString = "\(anotherDouble)"
print(anotherString) // 0.1
Apologies if this is an obvious question, but I built an app with Swift a while back and periodically make updates to it, but each time I come back to it I'm pretty rusty.
I have the following code that sorts items by time:
let dateFormat = DateFormatter()
dateFormat.dateFormat = "h:mm a"
items = items.sorted(by: {dateFormat.date(from: $0.time)! < dateFormat.date(from: $1.time)!})
This works fine for devices using 12-hour time, but throws an error for devices using 24-hour time (and possibly if the locale uses 12-hour time, but not AM/PM?).
Through reading various Stack Overflow questions and developer documentation, and from some trial and error, I've found that adding the following line of code fixes the issue:
dateFormat.locale = Locale(identifier: "en_US")
If I'm sorting by time and I've specified my date format, why does it matter what locale a device is using? When the DateFormatter documentation says:
... provides a representation of a specified date that is appropriate
for a given locale.
Does that mean every date formatter must have a locale specified and since I hadn't specified one in my original code it assumed the locale of the device? Therefore, if a user was using a locale that didn't support a 12-hour clock it threw an error?
An approach...
Since you have two possible formats, you need two formatters...
let format12 = DateFormatter()
format12.dateFormat = "h:mm a"
// Fix the possible formatting and avoid issues with the local and parsing
format12.locale = Locale(identifier: "en_US_POSIX")
let format24 = DateFormatter()
format24.dateFormat = "H:mm"
format24.locale = Locale(identifier: "en_US_POSIX")
Now you need to parse your input into date values...
var items: [String] = ["1:00 PM", "4:00 PM", "13:00", "16:00"]
var dates: [Date?] = items.map {
if let value = format24.date(from: $0) { return value }
else if let value = format12.date(from: $0) { return value }
return nil
}
And sort it...
dates.sort {
guard let lhs = $0, let rhs = $1 else { return true }
return lhs < rhs
}
Now, the above anticipates that some values may be nil and takes appropriate action.
Now you could, instead, use compactMap to remove the possible nil values, for example...
var dates: [Date] = items.compactMap {
if let value = format24.date(from: $0) { return value }
else if let value = format12.date(from: $0) { return value }
return nil
}
dates.sort {
guard let lhs = $0, let rhs = $1 else { return true }
return lhs < rhs
}
dates.sort { $0 < $1 }
I dumped all of this into Playground and have no issues
Side note...
If you are getting nil values with either of the two formatters, then you need to take a closer look at the values and determine how they are not fitting the pattern of the formatters and take appropriate actions to correct it
Instead of create Date object and sort
you can create timestamp and then sort it based on timestamp.
let userDefined = Measurement(value: Double(userInput.text!)!, unit: UnitMass.kilograms)
let calculatedValue = userDefined.converted(to: UnitMass.grams)
print(calculatedValue)
let formatter = MeasurementFormatter()
formatter.locale = Locale(identifier: "en_US")
Convertedunit.text = formatter.string(from: calculatedValue)
The user input is 5.
The output of print(calculatedValue) is 5000.0g.
However, the output of Convertedunit.text is 11.003lbs which is in pounds. I tried to use different methods, but it is still not in grams. Can anyone enlighten me?
Because of formatter.locale = Locale(identifier: "en_US"), the formatter will automatically convert everything to the imperial unit system, which in this case is pounds. Grams on the other hand belong to the metric system.
There are two ways to change this behaviour:
A) If you want to use the metric system specify a locale for a country that uses it, such as Germany. formatter.locale = Locale(identifier: "de_DE"). Don't worry, this will not affect the language of the string ( such as German: Meter, English: Meters, French: Mètres) as that is still bound to the apps language.
B) If you want to keep whatever unit you put into the formatter simply declare: formatter.unitOptions = .init(arrayLiteral: .providedUnit)
That way the formatter will generate strings with whatever unit you have provided it with.
I'm trying to read the user set system preferences for Temperature unit (Celsius/Fahrenheit).
I was trying to get this data using NSLocale but I cannot find any evidence of a temperature setting in there.
Is it even possible to read this data?
Thanks!
The official API is documented under the Preferences Utilities:
let key = "AppleTemperatureUnit" as CFString
let domain = "Apple Global Domain" as CFString
if let unit = CFPreferencesCopyValue(key, domain, kCFPreferencesCurrentUser, kCFPreferencesAnyHost) as? String {
print(unit)
} else {
print("Temperature unit not found")
}
If you wonder how I found it, I used the defaults utility in the Terminal:
> defaults find temperature
Found 1 keys in domain 'Apple Global Domain': {
AppleTemperatureUnit = Fahrenheit;
}
Found 1 keys in domain 'com.apple.ncplugin.weather': {
WPUseMetricTemperatureUnits = 1;
}
This is a bit of a hack, but you can do it this way on macOS 10.12+ and iOS 10+:
// Create a formatter.
let formatter = MeasurementFormatter()
// Create a dummy temperature, the unit doesn't matter because the formatter will localise it.
let dummyTemp = Measurement(value: 0, unit: UnitTemperature.celsius)
let unit = formatter.string(from: dummyTemp).characters.last // -> F
This outputs "F" in my playground which defaults to the US locale. But change your locale or use this code on a device and you'll get the locale specific temperature unit - or the string for it anyway.
In Swift when I create custom units I can only define one symbol. With the built in units there can be short, medium and long units. How do you set the other unit styles for a custom unit?
extension UnitEnergy {
static let footPounds = UnitEnergy(symbol: "ft-lbs", converter: UnitConverterLinear(coefficient: 1))
}
var test = Measurement<UnitEnergy>( value: 10, unit: .footPounds)
var formatter = MeasurementFormatter()
formatter.locale = Locale(identifier: "es")
formatter.unitStyle = .short
print( formatter.string(from: test))
formatter.unitStyle = .medium
print( formatter.string(from: test))
formatter.unitStyle = .long
print( formatter.string(from: test))
formatter.unitOptions = .providedUnit
formatter.unitStyle = .short
print( formatter.string(from: test))
formatter.unitStyle = .medium
print( formatter.string(from: test))
formatter.unitStyle = .long
print( formatter.string(from: test))
Output:
10 J
10 J
10 julios
10 ft-lbs
10 ft-lbs
10 ft-lbs
Short answer - you can't. The API does not provide any facility that allows you to provide different symbols for the three unit styles.
For custom units, the MeasurementFormatter only has the one symbol used when defining the custom unit.
Keep in mind that the need is for much more than just three different possible symbols for the three different unit styles. You would actually need three different string formats because some units might have a space or other punctuation, some might not. Some might appear before the value while some appear after the value.
And then there is the issue of localizing the unit. The Foundation framework provides all of this information for all supported languages so MeasurementFormatter can show all three unit styles for all supported languages for all predefined units.
Since the API does support custom units but not the ability to provide unit style specific symbols, I would suggest filing an enhancement request with Apple.
Have the same question, if there is any news please let me know. For now, I solved it like:
extension MeasurementFormatter {
func customString(from unit: Unit) -> String {
guard self.unitStyle == .long else { //I only needed .long but you get the idea
return self.string(from: unit)
}
switch unit {
case UnitEnergy.footPounds: return "foot-pounds"
default: return self.string(from: unit)
}
}