diff --git a/DarkSkyKit/TimeMachine.swift b/DarkSkyKit/TimeMachine.swift index 10add64..8844181 100644 --- a/DarkSkyKit/TimeMachine.swift +++ b/DarkSkyKit/TimeMachine.swift @@ -2,7 +2,7 @@ import Foundation import Alamofire public extension DarkSkyKit { - public func timeMachine(latitude lat: Double, lognitude long: Double, date: Date, result: @escaping (Result) -> Void) { + public func timeMachine(latitude lat: Double, longitude long: Double, date: Date, result: @escaping (Result) -> Void) { Alamofire.request(Router.timeMachine(configuration, lat, long, date)).responseJSON { response in switch response.result { case .success(let value): diff --git a/Example/DarkSkyKitExample.xcodeproj/project.pbxproj b/Example/DarkSkyKitExample.xcodeproj/project.pbxproj index 50b273e..1551c7d 100644 --- a/Example/DarkSkyKitExample.xcodeproj/project.pbxproj +++ b/Example/DarkSkyKitExample.xcodeproj/project.pbxproj @@ -22,14 +22,15 @@ 1E433E951D47923A0056479A /* ForecastFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E433E931D47923A0056479A /* ForecastFlags.swift */; }; 1E433E971D4792500056479A /* ForecastAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E433E961D4792500056479A /* ForecastAlert.swift */; }; 1E433E981D4792500056479A /* ForecastAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E433E961D4792500056479A /* ForecastAlert.swift */; }; - 1EA9820D1D48FF1900B3CAD3 /* ForecastFlagsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA9820C1D48FF1900B3CAD3 /* ForecastFlagsTests.swift */; }; - 1EA982111D48FFEB00B3CAD3 /* flags.json in Resources */ = {isa = PBXBuildFile; fileRef = 1EA982101D48FFEB00B3CAD3 /* flags.json */; }; + 1EA9820D1D48FF1900B3CAD3 /* ForecastCurrentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA9820C1D48FF1900B3CAD3 /* ForecastCurrentTests.swift */; }; + 1EA982111D48FFEB00B3CAD3 /* forecast.json in Resources */ = {isa = PBXBuildFile; fileRef = 1EA982101D48FFEB00B3CAD3 /* forecast.json */; }; 5525DC861D3D8ACA00D3967C /* DarkSkyKitExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5525DC7C1D3D8ACA00D3967C /* DarkSkyKitExample.framework */; }; 5525DC951D3D8C2E00D3967C /* DarkSkyKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5525DC941D3D8C2E00D3967C /* DarkSkyKit.swift */; }; 5525DC971D3D973A00D3967C /* Current.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5525DC961D3D973A00D3967C /* Current.swift */; }; 5525DC991D3D974700D3967C /* TimeMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5525DC981D3D974700D3967C /* TimeMachine.swift */; }; 5525DC9B1D3D9ECA00D3967C /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5525DC9A1D3D9ECA00D3967C /* Configuration.swift */; }; 5535ADED1D44270D002959EC /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5535ADEC1D44270D002959EC /* Router.swift */; }; + 555AF6DF1DC685EB00178012 /* ForecastTimeMachineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 555AF6DE1DC685EB00178012 /* ForecastTimeMachineTests.swift */; }; D5E32BD5105B038CA4A10B05 /* Pods_DarkSkyKitExampleTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 39C1E25F35A04708AEEBE999 /* Pods_DarkSkyKitExampleTests.framework */; }; F5ADC57F0B8F2D88A238781B /* Pods_DarkSkyKitExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4C2E930139FA5009D7F9220 /* Pods_DarkSkyKitExample.framework */; }; /* End PBXBuildFile section */ @@ -50,8 +51,8 @@ 1E433E901D4792250056479A /* ForecastDataPoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForecastDataPoint.swift; sourceTree = ""; }; 1E433E931D47923A0056479A /* ForecastFlags.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForecastFlags.swift; sourceTree = ""; }; 1E433E961D4792500056479A /* ForecastAlert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForecastAlert.swift; sourceTree = ""; }; - 1EA9820C1D48FF1900B3CAD3 /* ForecastFlagsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForecastFlagsTests.swift; sourceTree = ""; }; - 1EA982101D48FFEB00B3CAD3 /* flags.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = flags.json; sourceTree = ""; }; + 1EA9820C1D48FF1900B3CAD3 /* ForecastCurrentTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForecastCurrentTests.swift; sourceTree = ""; }; + 1EA982101D48FFEB00B3CAD3 /* forecast.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = forecast.json; sourceTree = ""; }; 21505191131C1AA1AC555975 /* Pods-DarkSkyKitExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DarkSkyKitExample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-DarkSkyKitExample/Pods-DarkSkyKitExample.debug.xcconfig"; sourceTree = ""; }; 39C1E25F35A04708AEEBE999 /* Pods_DarkSkyKitExampleTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DarkSkyKitExampleTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5525DC6C1D3D89FF00D3967C /* ConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationTests.swift; sourceTree = ""; }; @@ -63,6 +64,7 @@ 5525DC981D3D974700D3967C /* TimeMachine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeMachine.swift; sourceTree = ""; }; 5525DC9A1D3D9ECA00D3967C /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; 5535ADEC1D44270D002959EC /* Router.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; + 555AF6DE1DC685EB00178012 /* ForecastTimeMachineTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForecastTimeMachineTests.swift; sourceTree = ""; }; 55B43F361D8AD9640000B3D3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A4C2E930139FA5009D7F9220 /* Pods_DarkSkyKitExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DarkSkyKitExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DE43E9BF7E6793CD2ECD5389 /* Pods-DarkSkyKitExampleTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DarkSkyKitExampleTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-DarkSkyKitExampleTests/Pods-DarkSkyKitExampleTests.debug.xcconfig"; sourceTree = ""; }; @@ -105,7 +107,7 @@ 1EA9820F1D48FFB200B3CAD3 /* Data */ = { isa = PBXGroup; children = ( - 1EA982101D48FFEB00B3CAD3 /* flags.json */, + 1EA982101D48FFEB00B3CAD3 /* forecast.json */, ); name = Data; path = jsons; @@ -152,7 +154,8 @@ 1EA9820F1D48FFB200B3CAD3 /* Data */, 1E1FE0C71D47CF4D003A4AAE /* ForecastKitTests.swift */, 5525DC6C1D3D89FF00D3967C /* ConfigurationTests.swift */, - 1EA9820C1D48FF1900B3CAD3 /* ForecastFlagsTests.swift */, + 1EA9820C1D48FF1900B3CAD3 /* ForecastCurrentTests.swift */, + 555AF6DE1DC685EB00178012 /* ForecastTimeMachineTests.swift */, 5525DC6E1D3D89FF00D3967C /* Info.plist */, ); path = DarkSkyKitTests; @@ -291,7 +294,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 1EA982111D48FFEB00B3CAD3 /* flags.json in Resources */, + 1EA982111D48FFEB00B3CAD3 /* forecast.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -402,10 +405,11 @@ 1E1FE0C11D47C0B6003A4AAE /* Router.swift in Sources */, 1E433E921D4792250056479A /* ForecastDataPoint.swift in Sources */, 1E433E951D47923A0056479A /* ForecastFlags.swift in Sources */, - 1EA9820D1D48FF1900B3CAD3 /* ForecastFlagsTests.swift in Sources */, + 1EA9820D1D48FF1900B3CAD3 /* ForecastCurrentTests.swift in Sources */, 1E1FE0C41D47C0BE003A4AAE /* Configuration.swift in Sources */, 1E1FE0C51D47C0C0003A4AAE /* DarkSkyKit.swift in Sources */, 1E1FE0C31D47C0BB003A4AAE /* Current.swift in Sources */, + 555AF6DF1DC685EB00178012 /* ForecastTimeMachineTests.swift in Sources */, 1E1FE0C61D47C11C003A4AAE /* ConfigurationTests.swift in Sources */, 1E1FE0C81D47CF4D003A4AAE /* ForecastKitTests.swift in Sources */, ); diff --git a/Example/DarkSkyKitTests/ForecastCurrentTests.swift b/Example/DarkSkyKitTests/ForecastCurrentTests.swift new file mode 100644 index 0000000..6a68e4e --- /dev/null +++ b/Example/DarkSkyKitTests/ForecastCurrentTests.swift @@ -0,0 +1,67 @@ +import XCTest +import Foundation +import OHHTTPStubs + +class ForecastCurrentTests: XCTestCase { + + let client = DarkSkyKit(apiToken: "0") + + func testForecastRequest() { + stubAPIResponse() + let exp = expectation(description: "API Call") + client.current(latitude: 0.00, longitude: 0.00) { result in + switch result { + case .success(let data): + self.testForecastBasicInfo(fromData: data) + self.testForecastCurrently(fromData: data) + self.testForecastMinutely(fromData: data) + self.testForecastHourly(fromData: data) + self.testForecastDaily(fromData: data) + self.testForecastAlerts(fromData: data) + self.testForecastFlags(fromData: data) + default: break + } + exp.fulfill() + } + waitForExpectations(timeout: 5.0, handler: nil) + } + + func testForecastBasicInfo(fromData data: Forecast) { + XCTAssertEqual(data.latitude, 37.8267) + XCTAssertEqual(data.longitude, -122.4233) + XCTAssertEqual(data.timezone, "America/Los_Angeles") + XCTAssertEqual(data.offset, -7) + } + + func testForecastCurrently(fromData data: Forecast) { + XCTAssertEqual(data.currently?.summary, "Mostly Cloudy") + XCTAssertEqual(data.currently?.apparentTemperature, 57.34) + } + + func testForecastMinutely(fromData data: Forecast) { + XCTAssertEqual(data.minutely?.first?.time?.timeIntervalSince1970, 1477854360) + } + + func testForecastHourly(fromData data: Forecast) { + XCTAssertEqual(data.hourly?.first?.temperature, 57.02) + } + + func testForecastDaily(fromData data: Forecast) { + XCTAssertEqual(data.daily?.first?.precipIntensity, 0.0136) + } + + func testForecastAlerts(fromData data: Forecast) { + XCTAssertEqual(data.alerts?.first?.title, "Flood Watch for Mason, WA") + } + + func testForecastFlags(fromData data: Forecast) { + XCTAssertEqual(data.flags?.sources?.first, "darksky") + } + + func stubAPIResponse() { + stub(condition: isHost("api.darksky.net")) { _ in + let stubPath = OHPathForFile("forecast.json", type(of: self)) + return fixture(filePath: stubPath!, headers: ["Content-Type" as NSObject:"application/json" as AnyObject]) + } + } +} diff --git a/Example/DarkSkyKitTests/ForecastFlagsTests.swift b/Example/DarkSkyKitTests/ForecastFlagsTests.swift deleted file mode 100644 index 2b58dd0..0000000 --- a/Example/DarkSkyKitTests/ForecastFlagsTests.swift +++ /dev/null @@ -1,14 +0,0 @@ -import XCTest -import Foundation - -class ForecastFlagsTests: XCTestCase { - func testForecastFlagsMap() { - - if let path = Bundle.main.path(forResource: "flags", ofType: "json") { - let data = try! Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) - if let jsonResult: NSDictionary = try! JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary { - print(jsonResult) - } - } - } -} diff --git a/Example/DarkSkyKitTests/ForecastTimeMachineTests.swift b/Example/DarkSkyKitTests/ForecastTimeMachineTests.swift new file mode 100644 index 0000000..40e9b10 --- /dev/null +++ b/Example/DarkSkyKitTests/ForecastTimeMachineTests.swift @@ -0,0 +1,67 @@ +import XCTest +import Foundation +import OHHTTPStubs + +class ForecastTimeMachineTests: XCTestCase { + + let client = DarkSkyKit(apiToken: "0") + + func testForecastRequest() { + stubAPIResponse() + let exp = expectation(description: "API Call") + client.timeMachine(latitude: 0.00, longitude: 0.00, date: Date(timeIntervalSince1970: 1477854360)) { result in + switch result { + case .success(let data): + self.testForecastBasicInfo(fromData: data) + self.testForecastCurrently(fromData: data) + self.testForecastMinutely(fromData: data) + self.testForecastHourly(fromData: data) + self.testForecastDaily(fromData: data) + self.testForecastAlerts(fromData: data) + self.testForecastFlags(fromData: data) + default: break + } + exp.fulfill() + } + waitForExpectations(timeout: 5.0, handler: nil) + } + + func testForecastBasicInfo(fromData data: Forecast) { + XCTAssertEqual(data.latitude, 37.8267) + XCTAssertEqual(data.longitude, -122.4233) + XCTAssertEqual(data.timezone, "America/Los_Angeles") + XCTAssertEqual(data.offset, -7) + } + + func testForecastCurrently(fromData data: Forecast) { + XCTAssertEqual(data.currently?.summary, "Mostly Cloudy") + XCTAssertEqual(data.currently?.apparentTemperature, 57.34) + } + + func testForecastMinutely(fromData data: Forecast) { + XCTAssertEqual(data.minutely?.first?.time?.timeIntervalSince1970, 1477854360) + } + + func testForecastHourly(fromData data: Forecast) { + XCTAssertEqual(data.hourly?.first?.temperature, 57.02) + } + + func testForecastDaily(fromData data: Forecast) { + XCTAssertEqual(data.daily?.first?.precipIntensity, 0.0136) + } + + func testForecastAlerts(fromData data: Forecast) { + XCTAssertEqual(data.alerts?.first?.title, "Flood Watch for Mason, WA") + } + + func testForecastFlags(fromData data: Forecast) { + XCTAssertEqual(data.flags?.sources?.first, "darksky") + } + + func stubAPIResponse() { + stub(condition: isHost("api.darksky.net")) { _ in + let stubPath = OHPathForFile("forecast.json", type(of: self)) + return fixture(filePath: stubPath!, headers: ["Content-Type" as NSObject:"application/json" as AnyObject]) + } + } +} diff --git a/Example/DarkSkyKitTests/jsons/flags.json b/Example/DarkSkyKitTests/jsons/flags.json deleted file mode 100644 index 68544bc..0000000 --- a/Example/DarkSkyKitTests/jsons/flags.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "flags": { - "darksky-stations": [ - "1", - "2" - ], - "datapoint-stations": [ - "1", - "2" - ], - "lamp-stations": [ - "1", - "2" - ], - "metar-stations": [ - "1", - "2" - ], - "metno-license": [ - "1", - "2" - ], - "sources": [ - "gfs", - "cmc", - "fnmoc", - "isd", - "madis" - ], - "isd-stations": [ - "081763-99999", - "081800-99999", - "081810-99999", - "081820-99999", - "081860-99999" - ], - "madis-stations": [ - "IBCX5", - "LEBL", - "LELL", - "ZCDP8" - ], - "units": "si" - } -} diff --git a/Example/DarkSkyKitTests/jsons/forecast.json b/Example/DarkSkyKitTests/jsons/forecast.json new file mode 100644 index 0000000..31898cb --- /dev/null +++ b/Example/DarkSkyKitTests/jsons/forecast.json @@ -0,0 +1,160 @@ +{ + "latitude":37.8267, + "longitude":-122.4233, + "timezone":"America/Los_Angeles", + "offset":-7, + "currently":{"time":1477854386, + "summary":"Mostly Cloudy", + "icon":"partly-cloudy-day", + "nearestStormDistance":4, + "nearestStormBearing":284, + "precipIntensity":0, + "precipProbability":0, + "temperature":57.34, + "apparentTemperature":57.34, + "dewPoint":53.97, + "humidity":0.88, + "windSpeed":10.43, + "windBearing":211, + "visibility":5.92, + "cloudCover":0.92, + "pressure":1011.29, + "ozone":265.47}, + "minutely":{ + "summary":"Mostly cloudy for the hour.", + "icon":"partly-cloudy-day", + "data":[ + { + "time":1477854360, + "precipIntensity":0, + "precipProbability":0 + } + ] + }, + "hourly":{ + "summary":"Light rain until this evening.", + "icon":"rain", + "data":[ + {"time":1477854000, + "summary":"Light Rain", + "icon":"rain", + "precipIntensity":0.0138, + "precipProbability":0.61, + "precipType":"rain", + "temperature":57.02, + "apparentTemperature":57.02, + "dewPoint":53.77, + "humidity":0.89, + "windSpeed":10.22, + "windBearing":211, + "visibility":5.87, + "cloudCover":0.94, + "pressure":1011.26, + "ozone":265.27},{"time":1477857600, + "summary":"Light Rain", + "icon":"rain", + "precipIntensity":0.0139, + "precipProbability":0.53, + "precipType":"rain", + "temperature":59.98, + "apparentTemperature":59.98, + "dewPoint":55.53, + "humidity":0.85, + "windSpeed":12.21, + "windBearing":213, + "visibility":6.36, + "cloudCover":0.73, + "pressure":1011.53, + "ozone":267.17 + } + ] + }, + "daily":{ + "summary":"Light rain throughout the week, with temperatures rising to 72°F on Friday.", + "icon":"rain", + "data":[ + { + "time":1477810800, + "summary":"Rain until evening.", + "icon":"rain", + "sunriseTime":1477838092, + "sunsetTime":1477876452, + "moonPhase":0.01, + "precipIntensity":0.0136, + "precipIntensityMax":0.0919, + "precipIntensityMaxTime":1477850400, + "precipProbability":0.77, + "precipType":"rain", + "temperatureMin":56.48, + "temperatureMinTime":1477850400, + "temperatureMax":62.79, + "temperatureMaxTime":1477868400, + "apparentTemperatureMin":56.48, + "apparentTemperatureMinTime":1477850400, + "apparentTemperatureMax":62.79, + "apparentTemperatureMaxTime":1477868400, + "dewPoint":55.5, + "humidity":0.86, + "windSpeed":8.25, + "windBearing":204, + "visibility":7.09, + "cloudCover":0.66, + "pressure":1012.07, + "ozone":260.68 + } + ] + }, + "alerts": [ + { + "title": "Flood Watch for Mason, WA", + "time": 1453375020, + "expires": 1453407300, + "description": "...FLOOD WATCH REMAINS IN EFFECT THROUGH LATE FRIDAY.\n", + "uri": "http://alerts.weather.gov/cap/wwacapget.php?x=WA1255E4DB8494.FloodWatch.1255E4DCE35CWA.SEWFFASEW.38e78ec64613478bb70fc6ed9c87f6e6" + } + ], + "flags":{ + "sources":[ + "darksky", + "lamp", + "gfs", + "cmc", + "nam", + "rap", + "rtma", + "sref", + "fnmoc", + "isd", + "nwspa", + "madis", + "nearest-precip" + ], + "darksky-stations":[ + "KMUX" + ], + "lamp-stations":[ + "KAPC", + "KCCR", + "KHWD", + "KLVK", + "KNUQ", + "KOAK", + "KPAO", + "KSFO", + "KSQL" + ], + "isd-stations":[ + "724943-99999", + "745039-99999", + "745065-99999", + "994016-99999", + "998479-99999"], + "madis-stations":[ + "AU915", + "C5988", + "C8158", + "C9629", + "SFOC1"], + "units":"us"} +} + diff --git a/Example/Podfile b/Example/Podfile index b15e8e4..76ab828 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -7,6 +7,7 @@ target 'DarkSkyKitExample' do target 'DarkSkyKitExampleTests' do inherit! :search_paths - # Pods for testing - end + pod 'OHHTTPStubs', '~> 5.2.1' + pod 'OHHTTPStubs/Swift', '~> 5.2.1' + end end diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 4604341..49d9495 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,12 +1,30 @@ PODS: - Alamofire (4.0.0) + - OHHTTPStubs (5.2.2): + - OHHTTPStubs/Default (= 5.2.2) + - OHHTTPStubs/Core (5.2.2) + - OHHTTPStubs/Default (5.2.2): + - OHHTTPStubs/Core + - OHHTTPStubs/JSON + - OHHTTPStubs/NSURLSession + - OHHTTPStubs/OHPathHelpers + - OHHTTPStubs/JSON (5.2.2): + - OHHTTPStubs/Core + - OHHTTPStubs/NSURLSession (5.2.2): + - OHHTTPStubs/Core + - OHHTTPStubs/OHPathHelpers (5.2.2) + - OHHTTPStubs/Swift (5.2.2): + - OHHTTPStubs/Core DEPENDENCIES: - Alamofire (= 4.0.0) + - OHHTTPStubs (~> 5.2.1) + - OHHTTPStubs/Swift (~> 5.2.1) SPEC CHECKSUMS: Alamofire: fef59f00388f267e52d9b432aa5d93dc97190f14 + OHHTTPStubs: 34d9d0994e64fcf8552dbfade5ce82ada913ee31 -PODFILE CHECKSUM: 52031aac8ee284d495b690fb6c6d22cd5a5fe13a +PODFILE CHECKSUM: 269f7ad4a20725f5d61bba88decbc90cb8269424 COCOAPODS: 1.1.1