Skip to content

Commit 99c6656

Browse files
authored
Merge pull request #791 from kweinmeister/energyformatter
2 parents 6a7cc93 + 0920e69 commit 99c6656

File tree

4 files changed

+327
-20
lines changed

4 files changed

+327
-20
lines changed

Foundation.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@
312312
7900433C1CACD33E00ECCBF1 /* TestNSPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7900433A1CACD33E00ECCBF1 /* TestNSPredicate.swift */; };
313313
AE35A1861CBAC85E0042DB84 /* SwiftFoundation.h in Headers */ = {isa = PBXBuildFile; fileRef = AE35A1851CBAC85E0042DB84 /* SwiftFoundation.h */; settings = {ATTRIBUTES = (Public, ); }; };
314314
BD8042161E09857800487EB8 /* TestNSLengthFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8042151E09857800487EB8 /* TestNSLengthFormatter.swift */; };
315+
BDBB65901E256BFA001A7286 /* TestNSEnergyFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBB658F1E256BFA001A7286 /* TestNSEnergyFormatter.swift */; };
315316
BDFDF0A71DFF5B3E00C04CC5 /* TestNSPersonNameComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFDF0A61DFF5B3E00C04CC5 /* TestNSPersonNameComponents.swift */; };
316317
BF8E65311DC3B3CB005AB5C3 /* TestNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8E65301DC3B3CB005AB5C3 /* TestNotification.swift */; };
317318
CC5249C01D341D23007CB54D /* TestUnitConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC5249BF1D341D23007CB54D /* TestUnitConverter.swift */; };
@@ -755,6 +756,7 @@
755756
A5A34B551C18C85D00FD972B /* TestNSByteCountFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSByteCountFormatter.swift; sourceTree = "<group>"; };
756757
AE35A1851CBAC85E0042DB84 /* SwiftFoundation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SwiftFoundation.h; sourceTree = "<group>"; };
757758
BD8042151E09857800487EB8 /* TestNSLengthFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSLengthFormatter.swift; sourceTree = "<group>"; };
759+
BDBB658F1E256BFA001A7286 /* TestNSEnergyFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSEnergyFormatter.swift; sourceTree = "<group>"; };
758760
BDFDF0A61DFF5B3E00C04CC5 /* TestNSPersonNameComponents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSPersonNameComponents.swift; sourceTree = "<group>"; };
759761
BF8E65301DC3B3CB005AB5C3 /* TestNotification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNotification.swift; sourceTree = "<group>"; };
760762
C2A9D75B1C15C08B00993803 /* TestNSUUID.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSUUID.swift; sourceTree = "<group>"; };
@@ -1346,6 +1348,7 @@
13461348
2EBE67A31C77BF05006583D5 /* TestNSDateFormatter.swift */,
13471349
231503DA1D8AEE5D0061694D /* TestNSDecimal.swift */,
13481350
EA66F63D1BF1619600136161 /* TestNSDictionary.swift */,
1351+
BDBB658F1E256BFA001A7286 /* TestNSEnergyFormatter.swift */,
13491352
D512D17B1CD883F00032E6A5 /* TestNSFileHandle.swift */,
13501353
525AECEB1BF2C96400D15BB0 /* TestNSFileManager.swift */,
13511354
88D28DE61C13AE9000494606 /* TestNSGeometry.swift */,
@@ -2267,6 +2270,7 @@
22672270
5B13B3291C582D4C00651CE2 /* TestNSCalendar.swift in Sources */,
22682271
5B13B34F1C582D4C00651CE2 /* TestNSXMLParser.swift in Sources */,
22692272
D5C40F331CDA1D460005690C /* TestNSOperationQueue.swift in Sources */,
2273+
BDBB65901E256BFA001A7286 /* TestNSEnergyFormatter.swift in Sources */,
22702274
5B13B32F1C582D4C00651CE2 /* TestNSGeometry.swift in Sources */,
22712275
EA08126C1DA810BE00651B70 /* ProgressFraction.swift in Sources */,
22722276
5B13B3351C582D4C00651CE2 /* TestNSKeyedUnarchiver.swift in Sources */,

Foundation/NSEnergyFormatter.swift

Lines changed: 174 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,41 +7,195 @@
77
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
88
//
99

10-
1110
extension EnergyFormatter {
12-
public enum Unit : Int {
11+
public enum Unit: Int {
12+
13+
case joule = 11
14+
case kilojoule = 14
15+
case calorie = 1793 // chemistry "calories", abbr "cal"
16+
case kilocalorie = 1794 // kilocalories in general, abbr “kcal”, or “C” in some locales (e.g. US) when usesFoodEnergy is set to YES
17+
18+
// Map Unit to UnitEnergy class to aid with conversions
19+
fileprivate var unitEnergy: UnitEnergy {
20+
switch self {
21+
case .joule:
22+
return UnitEnergy.joules
23+
case .kilojoule:
24+
return UnitEnergy.kilojoules
25+
case .calorie:
26+
return UnitEnergy.calories
27+
case .kilocalorie:
28+
return UnitEnergy.kilocalories
29+
}
30+
}
31+
32+
// Reuse symbols defined in UnitEnergy, except for kilocalories, which is defined as "kCal"
33+
fileprivate var symbol: String {
34+
switch self {
35+
case .kilocalorie:
36+
return "kcal"
37+
default:
38+
return unitEnergy.symbol
39+
}
40+
}
41+
42+
// Return singular, full string representation of the energy unit
43+
fileprivate var singularString: String {
44+
switch self {
45+
case .joule:
46+
return "joule"
47+
case .kilojoule:
48+
return "kilojoule"
49+
case .calorie:
50+
return "calorie"
51+
case .kilocalorie:
52+
return "kilocalorie"
53+
}
54+
}
1355

14-
case joule
15-
case kilojoule
16-
case calorie // chemistry "calories", abbr "cal"
17-
case kilocalorie // kilocalories in general, abbr “kcal”, or “C” in some locales (e.g. US) when usesFoodEnergy is set to YES
56+
// Return plural, full string representation of the energy unit
57+
fileprivate var pluralString: String {
58+
return "\(self.singularString)s"
59+
}
1860
}
1961
}
2062

21-
open class EnergyFormatter : Formatter {
22-
63+
open class EnergyFormatter: Formatter {
64+
65+
public override init() {
66+
numberFormatter = NumberFormatter()
67+
numberFormatter.numberStyle = .decimal
68+
unitStyle = .medium
69+
isForFoodEnergyUse = false
70+
super.init()
71+
}
72+
2373
public required init?(coder: NSCoder) {
24-
NSUnimplemented()
74+
numberFormatter = NumberFormatter()
75+
numberFormatter.numberStyle = .decimal
76+
unitStyle = .medium
77+
isForFoodEnergyUse = false
78+
super.init()
2579
}
26-
80+
2781
/*@NSCopying*/ open var numberFormatter: NumberFormatter! // default is NSNumberFormatter with NSNumberFormatterDecimalStyle
2882
open var unitStyle: UnitStyle // default is NSFormattingUnitStyleMedium
2983
open var isForFoodEnergyUse: Bool // default is NO; if it is set to YES, NSEnergyFormatterUnitKilocalorie may be “C” instead of “kcal"
30-
84+
3185
// Format a combination of a number and an unit to a localized string.
32-
open func string(fromValue value: Double, unit: Unit) -> String { NSUnimplemented() }
33-
86+
open func string(fromValue value: Double, unit: Unit) -> String {
87+
guard let formattedValue = numberFormatter.string(from:NSNumber(value: value)) else {
88+
fatalError("Cannot format \(value) as string")
89+
}
90+
91+
let separator = unitStyle == EnergyFormatter.UnitStyle.short ? "" : " "
92+
return "\(formattedValue)\(separator)\(unitString(fromValue: value, unit: unit))"
93+
}
94+
3495
// Format a number in joules to a localized string with the locale-appropriate unit and an appropriate scale (e.g. 10.3J = 2.46cal in the US locale).
35-
open func string(fromJoules numberInJoules: Double) -> String { NSUnimplemented() }
36-
96+
open func string(fromJoules numberInJoules: Double) -> String {
97+
98+
//Convert to the locale-appropriate unit
99+
var unitFromJoules: EnergyFormatter.Unit = .joule
100+
_ = self.unitString(fromJoules: numberInJoules, usedUnit: &unitFromJoules)
101+
102+
//Map the unit to UnitLength type for conversion later
103+
let unitEnergyFromJoules = unitFromJoules.unitEnergy
104+
105+
//Create a measurement object based on the value in joules
106+
let joulesMeasurement = Measurement<UnitEnergy>(value:numberInJoules, unit: .joules)
107+
108+
//Convert the object to the locale-appropriate unit determined above
109+
let unitMeasurement = joulesMeasurement.converted(to: unitEnergyFromJoules)
110+
111+
//Extract the number from the measurement
112+
let numberInUnit = unitMeasurement.value
113+
114+
return string(fromValue: numberInUnit, unit: unitFromJoules)
115+
}
116+
37117
// Return a localized string of the given unit, and if the unit is singular or plural is based on the given number.
38-
open func unitString(fromValue value: Double, unit: Unit) -> String { NSUnimplemented() }
39-
118+
open func unitString(fromValue value: Double, unit: Unit) -> String {
119+
120+
//Special case when isForFoodEnergyUse is true
121+
if isForFoodEnergyUse && unit == .kilocalorie {
122+
if unitStyle == .short {
123+
return "C"
124+
} else if unitStyle == .medium {
125+
return "Cal"
126+
} else {
127+
return "Calories"
128+
}
129+
}
130+
131+
if unitStyle == .short || unitStyle == .medium {
132+
return unit.symbol
133+
} else if value == 1.0 {
134+
return unit.singularString
135+
} else {
136+
return unit.pluralString
137+
}
138+
}
139+
40140
// Return the locale-appropriate unit, the same unit used by -stringFromJoules:.
41-
open func unitString(fromJoules numberInJoules: Double, usedUnit unitp: UnsafeMutablePointer<Unit>?) -> String { NSUnimplemented() }
42-
43-
141+
open func unitString(fromJoules numberInJoules: Double, usedUnit unitp: UnsafeMutablePointer<Unit>?) -> String {
142+
143+
//Convert to the locale-appropriate unit
144+
let unitFromJoules: Unit
145+
146+
if numberFormatter.locale.usesCalories {
147+
if numberInJoules > 0 && numberInJoules <= 4184 {
148+
unitFromJoules = .calorie
149+
} else {
150+
unitFromJoules = .kilocalorie
151+
}
152+
} else {
153+
if numberInJoules > 0 && numberInJoules <= 1000 {
154+
unitFromJoules = .joule
155+
} else {
156+
unitFromJoules = .kilojoule
157+
}
158+
}
159+
unitp?.pointee = unitFromJoules
160+
161+
//Map the unit to UnitEnergy type for conversion later
162+
let unitEnergyFromJoules = unitFromJoules.unitEnergy
163+
164+
//Create a measurement object based on the value in joules
165+
let joulesMeasurement = Measurement<UnitEnergy>(value:numberInJoules, unit: .joules)
166+
167+
//Convert the object to the locale-appropriate unit determined above
168+
let unitMeasurement = joulesMeasurement.converted(to: unitEnergyFromJoules)
169+
170+
//Extract the number from the measurement
171+
let numberInUnit = unitMeasurement.value
172+
173+
//Return the appropriate representation of the unit based on the selected unit style
174+
return unitString(fromValue: numberInUnit, unit: unitFromJoules)
175+
}
176+
44177
/// - Experiment: This is a draft API currently under consideration for official import into Foundation as a suitable alternative
45178
/// - Note: Since this API is under consideration it may be either removed or revised in the near future
46179
open override func objectValue(_ string: String) throws -> Any? { return nil }
47180
}
181+
182+
/// TODO: Replace calls to the below function to use Locale.regionCode
183+
/// Temporary workaround due to unpopulated Locale attributes
184+
/// See https://bugs.swift.org/browse/SR-3202
185+
extension Locale {
186+
public var usesCalories: Bool {
187+
188+
switch self.identifier {
189+
case "en_US": return true
190+
case "en_US_POSIX": return true
191+
case "haw_US": return true
192+
case "es_US": return true
193+
case "chr_US": return true
194+
case "en_GB": return true
195+
case "kw_GB": return true
196+
case "cy_GB": return true
197+
case "gv_GB": return true
198+
default: return false
199+
}
200+
}
201+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See http://swift.org/LICENSE.txt for license information
7+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
8+
//
9+
10+
#if DEPLOYMENT_RUNTIME_OBJC || os(Linux)
11+
import Foundation
12+
import XCTest
13+
#else
14+
import SwiftFoundation
15+
import SwiftXCTest
16+
#endif
17+
18+
class TestNSEnergyFormatter: XCTestCase {
19+
let formatter: EnergyFormatter = EnergyFormatter()
20+
21+
static var allTests: [(String, (TestNSEnergyFormatter) -> () throws -> Void)] {
22+
return [
23+
("test_stringFromJoulesJoulesRegion", test_stringFromJoulesJoulesRegion),
24+
("test_stringFromJoulesCaloriesRegion", test_stringFromJoulesCaloriesRegion),
25+
("test_stringFromJoulesCaloriesRegionFoodEnergyUse", test_stringFromJoulesCaloriesRegionFoodEnergyUse),
26+
("test_stringFromValue", test_stringFromValue),
27+
("test_unitStringFromValue", test_unitStringFromValue),
28+
("test_unitStringFromJoules", test_unitStringFromJoules)
29+
]
30+
}
31+
32+
override func setUp() {
33+
formatter.numberFormatter.locale = Locale(identifier: "en_US")
34+
formatter.isForFoodEnergyUse = false
35+
super.setUp()
36+
}
37+
38+
func test_stringFromJoulesJoulesRegion() {
39+
formatter.numberFormatter.locale = Locale(identifier: "de_DE")
40+
XCTAssertEqual(formatter.string(fromJoules: -100000), "-100 kJ")
41+
XCTAssertEqual(formatter.string(fromJoules: -1), "-0,001 kJ")
42+
XCTAssertEqual(formatter.string(fromJoules: 100000000), "100.000 kJ")
43+
}
44+
45+
46+
func test_stringFromJoulesCaloriesRegion() {
47+
XCTAssertEqual(formatter.string(fromJoules: -10000), "-2.39 kcal")
48+
XCTAssertEqual(formatter.string(fromJoules: 0.00001), "0 cal")
49+
XCTAssertEqual(formatter.string(fromJoules: 0.0001), "0 cal")
50+
XCTAssertEqual(formatter.string(fromJoules: 1), "0.239 cal")
51+
XCTAssertEqual(formatter.string(fromJoules: 10000), "2.39 kcal")
52+
}
53+
54+
func test_stringFromJoulesCaloriesRegionFoodEnergyUse() {
55+
formatter.isForFoodEnergyUse = true
56+
XCTAssertEqual(formatter.string(fromJoules: -1), "-0 Cal")
57+
XCTAssertEqual(formatter.string(fromJoules: 0.001), "0 cal")
58+
XCTAssertEqual(formatter.string(fromJoules: 0.1), "0.024 cal")
59+
XCTAssertEqual(formatter.string(fromJoules: 1), "0.239 cal")
60+
XCTAssertEqual(formatter.string(fromJoules: 10000), "2.39 Cal")
61+
}
62+
63+
func test_stringFromValue() {
64+
formatter.unitStyle = Formatter.UnitStyle.long
65+
XCTAssertEqual(formatter.string(fromValue: 0.002, unit: EnergyFormatter.Unit.kilojoule),"0.002 kilojoules")
66+
XCTAssertEqual(formatter.string(fromValue:0, unit:EnergyFormatter.Unit.joule), "0 joules")
67+
XCTAssertEqual(formatter.string(fromValue:1, unit:EnergyFormatter.Unit.joule), "1 joule")
68+
69+
formatter.unitStyle = Formatter.UnitStyle.short
70+
XCTAssertEqual(formatter.string(fromValue: 0.00000001, unit:EnergyFormatter.Unit.kilocalorie), "0kcal")
71+
XCTAssertEqual(formatter.string(fromValue: 2.4, unit: EnergyFormatter.Unit.calorie), "2.4cal")
72+
XCTAssertEqual(formatter.string(fromValue: 123456, unit: EnergyFormatter.Unit.calorie), "123,456cal")
73+
74+
formatter.unitStyle = Formatter.UnitStyle.medium
75+
formatter.isForFoodEnergyUse = true
76+
XCTAssertEqual(formatter.string(fromValue: 0.00000001, unit: EnergyFormatter.Unit.calorie), "0 cal")
77+
XCTAssertEqual(formatter.string(fromValue: 987654321, unit: EnergyFormatter.Unit.kilocalorie), "987,654,321 Cal")
78+
79+
formatter.isForFoodEnergyUse = false
80+
XCTAssertEqual(formatter.string(fromValue: 5.3, unit: EnergyFormatter.Unit.kilocalorie), "5.3 kcal")
81+
XCTAssertEqual(formatter.string(fromValue: 873.2345, unit: EnergyFormatter.Unit.calorie), "873.234 cal")
82+
}
83+
84+
func test_unitStringFromJoules() {
85+
var unit = EnergyFormatter.Unit.joule
86+
XCTAssertEqual(formatter.unitString(fromJoules: -100000, usedUnit: &unit), "kcal")
87+
XCTAssertEqual(unit, EnergyFormatter.Unit.kilocalorie)
88+
89+
XCTAssertEqual(formatter.unitString(fromJoules: 0, usedUnit: &unit), "kcal")
90+
XCTAssertEqual(unit, EnergyFormatter.Unit.kilocalorie)
91+
92+
XCTAssertEqual(formatter.unitString(fromJoules: 0.0001, usedUnit: &unit), "cal")
93+
XCTAssertEqual(unit, EnergyFormatter.Unit.calorie)
94+
95+
XCTAssertEqual(formatter.unitString(fromJoules: 4184, usedUnit: &unit), "cal")
96+
XCTAssertEqual(unit, EnergyFormatter.Unit.calorie)
97+
98+
XCTAssertEqual(formatter.unitString(fromJoules: 4185, usedUnit: &unit), "kcal")
99+
XCTAssertEqual(unit, EnergyFormatter.Unit.kilocalorie)
100+
101+
XCTAssertEqual(formatter.unitString(fromJoules: 100000, usedUnit: &unit), "kcal")
102+
XCTAssertEqual(unit, EnergyFormatter.Unit.kilocalorie)
103+
104+
formatter.numberFormatter.locale = Locale(identifier: "de_DE")
105+
XCTAssertEqual(formatter.unitString(fromJoules: -100000, usedUnit: &unit), "kJ")
106+
XCTAssertEqual(unit, EnergyFormatter.Unit.kilojoule)
107+
108+
XCTAssertEqual(formatter.unitString(fromJoules: 0, usedUnit: &unit), "kJ")
109+
XCTAssertEqual(unit, EnergyFormatter.Unit.kilojoule)
110+
111+
XCTAssertEqual(formatter.unitString(fromJoules: 0.0001, usedUnit: &unit), "J")
112+
XCTAssertEqual(unit, EnergyFormatter.Unit.joule)
113+
114+
XCTAssertEqual(formatter.unitString(fromJoules: 1000, usedUnit: &unit), "J")
115+
XCTAssertEqual(unit, EnergyFormatter.Unit.joule)
116+
117+
XCTAssertEqual(formatter.unitString(fromJoules: 1000.01, usedUnit: &unit), "kJ")
118+
XCTAssertEqual(unit, EnergyFormatter.Unit.kilojoule)
119+
}
120+
121+
func test_unitStringFromValue() {
122+
formatter.isForFoodEnergyUse = true
123+
formatter.unitStyle = Formatter.UnitStyle.long
124+
XCTAssertEqual(formatter.unitString(fromValue: 1, unit: EnergyFormatter.Unit.kilocalorie), "Calories")
125+
XCTAssertEqual(formatter.unitString(fromValue: 2, unit: EnergyFormatter.Unit.kilocalorie), "Calories")
126+
127+
formatter.unitStyle = Formatter.UnitStyle.medium
128+
XCTAssertEqual(formatter.unitString(fromValue: 0.00000001, unit: EnergyFormatter.Unit.kilocalorie), "Cal")
129+
XCTAssertEqual(formatter.unitString(fromValue: 987654321, unit: EnergyFormatter.Unit.kilocalorie), "Cal")
130+
131+
formatter.unitStyle = Formatter.UnitStyle.short
132+
XCTAssertEqual(formatter.unitString(fromValue: 0.00000001, unit: EnergyFormatter.Unit.calorie), "cal")
133+
XCTAssertEqual(formatter.unitString(fromValue: 123456, unit: EnergyFormatter.Unit.kilocalorie), "C")
134+
135+
formatter.isForFoodEnergyUse = false
136+
formatter.unitStyle = Formatter.UnitStyle.long
137+
XCTAssertEqual(formatter.unitString(fromValue: 0.002, unit: EnergyFormatter.Unit.kilojoule), "kilojoules")
138+
139+
formatter.unitStyle = Formatter.UnitStyle.medium
140+
XCTAssertEqual(formatter.unitString(fromValue: 0.00000001, unit: EnergyFormatter.Unit.kilocalorie), "kcal")
141+
XCTAssertEqual(formatter.unitString(fromValue: 987654321, unit: EnergyFormatter.Unit.kilocalorie), "kcal")
142+
143+
formatter.unitStyle = Formatter.UnitStyle.short
144+
XCTAssertEqual(formatter.unitString(fromValue: 0.00000001, unit: EnergyFormatter.Unit.calorie), "cal")
145+
XCTAssertEqual(formatter.unitString(fromValue: 123456, unit: EnergyFormatter.Unit.joule), "J")
146+
}
147+
148+
}

0 commit comments

Comments
 (0)