import Foundation @testable import App import XCTest import Vapor import SwiftEccodes final class OutputformatTests: XCTestCase { var app: Application? override func setUp() async throws { app = try Application.testable() } override func tearDown() async throws { try await app?.asyncShutdown() app = nil } /*func testBz2Grib() async throws { let url = "http://opendata.dwd.de/weather/nwp/icon-d2/grib/06/relhum/icon-d2_germany_regular-lat-lon_pressure-level_2022101306_004_500_relhum.grib2.bz2" let curl = Curl(logger: app!.logger) let grib = try await curl.downloadBz2Grib(url: url, client: app!.http.client.shared) try grib.forEach({message in message.iterate(namespace: .ls).forEach({ print($0) }) message.iterate(namespace: .geography).forEach({ print($0) }) print(message.get(attribute: "name")!) let data = try message.getDouble() print(data.count) print(data[0..<10]) }) }*/ /// Test adjustment of API call weights /// "Heavy" API calls are counted more than just 1 API call /// /// See: https://github.com/open-meteo/open-meteo/issues/438#issuecomment-1722945326 func testApiWeight() { let dataContainer = ForecastapiResult.PerModel( model: .best_match, latitude: 41, longitude: 2, elevation: nil, //timezone: .init(utcOffsetSeconds: 3600, identifier: "GMT", abbreviation: "GMT"), //time: TimerangeLocal(range: Timestamp(2000, 1, 1)...PerLocation(timezone: .init(utcOffsetSeconds: 3600, identifier: "GMT", abbreviation: "GMT"), time: TimerangeLocal(range: Timestamp(2000, 1, 1)..(timeformat: .iso8601, results: [location20year]) // 20 year data, one location, one variable XCTAssertEqual(result20year.calculateQueryWeight(nVariablesModels: 1), 54.79286) // 20 year data, one location, two variables XCTAssertEqual(result20year.calculateQueryWeight(nVariablesModels: 2), 109.58572) let location7day = ForecastapiResult.PerLocation(timezone: .init(utcOffsetSeconds: 3600, identifier: "GMT", abbreviation: "GMT"), time: TimerangeLocal(range: Timestamp(2000, 1, 1)..(timeformat: .iso8601, results: [location7day]) // 7 day data, one location, one variable XCTAssertEqual(result7day.calculateQueryWeight(nVariablesModels: 1), 1) // 7 day data, one location, two variables XCTAssertEqual(result7day.calculateQueryWeight(nVariablesModels: 2), 1) // 7 day data, one location, 15 variables XCTAssertEqual(result7day.calculateQueryWeight(nVariablesModels: 15), 1.5) // 7 day data, one location, 30 variables XCTAssertEqual(result7day.calculateQueryWeight(nVariablesModels: 30), 3) let location1month = ForecastapiResult.PerLocation(timezone: .init(utcOffsetSeconds: 3600, identifier: "GMT", abbreviation: "GMT"), time: TimerangeLocal(range: Timestamp(2000, 1, 1)..(timeformat: .iso8601, results: [location1month, location1month]) // 1 month data, two locations, one variable XCTAssertEqual(result1month.calculateQueryWeight(nVariablesModels: 1), 2.0) // 1 month data, two locations, two variables XCTAssertEqual(result1month.calculateQueryWeight(nVariablesModels: 2), 2.0) // 1 month data, two locations, 15 variables XCTAssertEqual(result1month.calculateQueryWeight(nVariablesModels: 15), 6.6428566) // 1 month data, two locations, 30 variables XCTAssertEqual(result1month.calculateQueryWeight(nVariablesModels: 30), 13.285713) } func drainString(_ response: Response) -> String { guard var buffer = try? response.body.collect(on: self.app!.eventLoopGroup.next()).wait() else { fatalError("could not get buffer") } guard let string = buffer.readString(length: buffer.writerIndex) else { fatalError("could not convert to string") } return string } func drainData(_ response: Response) -> Data { guard var buffer = try? response.body.collect(on: self.app!.eventLoopGroup.next()).wait() else { fatalError("could not get byffer") } guard let data = buffer.readData(length: buffer.writerIndex) else { fatalError("could not convert to data") } return data } func testFormats() async throws { /*let current = ForecastapiResult.CurrentWeather( temperature: 23, windspeed: 12, winddirection: 90, weathercode: 5, is_day: 1, temperature_unit: .celsius, windspeed_unit: .kmh, winddirection_unit: .degreeDirection, weathercode_unit: .wmoCode, time: Timestamp(2022,7,13,15,0))*/ let daily = ApiSection(name: "daily", time: TimerangeDt(start: Timestamp(2022,7,12,0), nTime: 2, dtSeconds: 86400), columns: [ ApiColumn(variable: .temperature_2m_mean, unit: .celsius, variables: [.float(.init(repeating: 20, count: 2))]), ApiColumn(variable: .windspeed_10m_mean, unit: .kilometresPerHour, variables: [.float(.init(repeating: 10, count: 2))]), ]) let hourly = ApiSection.SurfacePressureAndHeightVariable>(name: "hourly", time: TimerangeDt(start: Timestamp(2022,7,12,0), nTime: 48, dtSeconds: 3600), columns: [ ApiColumn(variable: .surface(.init(.temperature_2m, 0)), unit: .celsius, variables: [.float(.init(repeating: 20, count: 48))]), ApiColumn(variable: .surface(.init(.windspeed_10m, 0)), unit: .kilometresPerHour, variables: [.float(.init(repeating: 10, count: 48))]), ]) let currentSection = ApiSectionSingle.SurfacePressureAndHeightVariable>(name: "current_weather", time: Timestamp(2022,7,12,1,15), dtSeconds: 3600/4, columns: [ ApiColumnSingle(variable: .surface(.init(.temperature_20m, 0)), unit: .celsius, value: 20), ApiColumnSingle(variable: .surface(.init(.windspeed_100m, 0)), unit: .kilometresPerHour, value: 10), ]) let numberOfLocationsMaximum: (numberOfLocations: Int, apikey: String?) = (1000, nil) let res = ForecastapiResult.PerModel( model: .best_match, latitude: 41, longitude: 2, elevation: nil, prefetch: {}, current: {currentSection}, hourly: { hourly }, daily: { daily }, sixHourly: nil, minutely15: nil ) let data = ForecastapiResult(timeformat: .iso8601, results: [ForecastapiResult.PerLocation(timezone: .init(utcOffsetSeconds: 3600, identifier: "GMT", abbreviation: "GMT"), time: TimerangeLocal(range: daily.time.range, utcOffsetSeconds: 0), locationId: 0, results: [res])]) XCTAssertEqual(data.calculateQueryWeight(nVariablesModels: 2), 1) XCTAssertEqual(data.calculateQueryWeight(nVariablesModels: 15), 1.5) XCTAssertEqual(data.calculateQueryWeight(nVariablesModels: 20), 2) let json = await drainString(try data.response(format: .json, numberOfLocationsMaximum: numberOfLocationsMaximum, fixedGenerationTime: 12)) XCTAssertEqual(json, """ {"latitude":41.0,"longitude":2.0,"generationtime_ms":12.0,"utc_offset_seconds":3600,"timezone":"GMT","timezone_abbreviation":"GMT","current_weather_units":{"time":"iso8601","interval":"seconds","temperature_20m":"°C","windspeed_100m":"km/h"},"current_weather":{"time":"2022-07-12T02:15","interval":900,"temperature_20m":20.0,"windspeed_100m":10.0},"hourly_units":{"time":"iso8601","temperature_2m":"°C","windspeed_10m":"km/h"},"hourly":{"time":["2022-07-12T01:00","2022-07-12T02:00","2022-07-12T03:00","2022-07-12T04:00","2022-07-12T05:00","2022-07-12T06:00","2022-07-12T07:00","2022-07-12T08:00","2022-07-12T09:00","2022-07-12T10:00","2022-07-12T11:00","2022-07-12T12:00","2022-07-12T13:00","2022-07-12T14:00","2022-07-12T15:00","2022-07-12T16:00","2022-07-12T17:00","2022-07-12T18:00","2022-07-12T19:00","2022-07-12T20:00","2022-07-12T21:00","2022-07-12T22:00","2022-07-12T23:00","2022-07-13T00:00","2022-07-13T01:00","2022-07-13T02:00","2022-07-13T03:00","2022-07-13T04:00","2022-07-13T05:00","2022-07-13T06:00","2022-07-13T07:00","2022-07-13T08:00","2022-07-13T09:00","2022-07-13T10:00","2022-07-13T11:00","2022-07-13T12:00","2022-07-13T13:00","2022-07-13T14:00","2022-07-13T15:00","2022-07-13T16:00","2022-07-13T17:00","2022-07-13T18:00","2022-07-13T19:00","2022-07-13T20:00","2022-07-13T21:00","2022-07-13T22:00","2022-07-13T23:00","2022-07-14T00:00"],"temperature_2m":[20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0],"windspeed_10m":[10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0]},"daily_units":{"time":"iso8601","temperature_2m_mean":"°C","windspeed_10m_mean":"km/h"},"daily":{"time":["2022-07-12","2022-07-13"],"temperature_2m_mean":[20.0,20.0],"windspeed_10m_mean":[10.0,10.0]}} """) let dataUnix = ForecastapiResult(timeformat: .unixtime, results: [ForecastapiResult.PerLocation(timezone: .init(utcOffsetSeconds: 3600, identifier: "GMT", abbreviation: "GMT"), time: TimerangeLocal(range: daily.time.range, utcOffsetSeconds: 0), locationId: 0, results: [res])]) let jsonUnix = await drainString(try dataUnix.response(format: .json, numberOfLocationsMaximum: numberOfLocationsMaximum, fixedGenerationTime: 12)) XCTAssertEqual(jsonUnix, """ {"latitude":41.0,"longitude":2.0,"generationtime_ms":12.0,"utc_offset_seconds":3600,"timezone":"GMT","timezone_abbreviation":"GMT","current_weather_units":{"time":"unixtime","interval":"seconds","temperature_20m":"°C","windspeed_100m":"km/h"},"current_weather":{"time":1657588500,"interval":900,"temperature_20m":20.0,"windspeed_100m":10.0},"hourly_units":{"time":"unixtime","temperature_2m":"°C","windspeed_10m":"km/h"},"hourly":{"time":[1657584000,1657587600,1657591200,1657594800,1657598400,1657602000,1657605600,1657609200,1657612800,1657616400,1657620000,1657623600,1657627200,1657630800,1657634400,1657638000,1657641600,1657645200,1657648800,1657652400,1657656000,1657659600,1657663200,1657666800,1657670400,1657674000,1657677600,1657681200,1657684800,1657688400,1657692000,1657695600,1657699200,1657702800,1657706400,1657710000,1657713600,1657717200,1657720800,1657724400,1657728000,1657731600,1657735200,1657738800,1657742400,1657746000,1657749600,1657753200],"temperature_2m":[20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0],"windspeed_10m":[10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0]},"daily_units":{"time":"unixtime","temperature_2m_mean":"°C","windspeed_10m_mean":"km/h"},"daily":{"time":[1657584000,1657670400],"temperature_2m_mean":[20.0,20.0],"windspeed_10m_mean":[10.0,10.0]}} """) let csv = await drainString(try data.response(format: .csv, numberOfLocationsMaximum: numberOfLocationsMaximum)) XCTAssertEqual(csv, """ latitude,longitude,elevation,utc_offset_seconds,timezone,timezone_abbreviation 41.0,2.0,NaN,3600,GMT,GMT time,temperature_20m (°C),windspeed_100m (km/h) 2022-07-12T02:15,20.0,10.0 time,temperature_2m (°C),windspeed_10m (km/h) 2022-07-12T01:00,20.0,10.0 2022-07-12T02:00,20.0,10.0 2022-07-12T03:00,20.0,10.0 2022-07-12T04:00,20.0,10.0 2022-07-12T05:00,20.0,10.0 2022-07-12T06:00,20.0,10.0 2022-07-12T07:00,20.0,10.0 2022-07-12T08:00,20.0,10.0 2022-07-12T09:00,20.0,10.0 2022-07-12T10:00,20.0,10.0 2022-07-12T11:00,20.0,10.0 2022-07-12T12:00,20.0,10.0 2022-07-12T13:00,20.0,10.0 2022-07-12T14:00,20.0,10.0 2022-07-12T15:00,20.0,10.0 2022-07-12T16:00,20.0,10.0 2022-07-12T17:00,20.0,10.0 2022-07-12T18:00,20.0,10.0 2022-07-12T19:00,20.0,10.0 2022-07-12T20:00,20.0,10.0 2022-07-12T21:00,20.0,10.0 2022-07-12T22:00,20.0,10.0 2022-07-12T23:00,20.0,10.0 2022-07-13T00:00,20.0,10.0 2022-07-13T01:00,20.0,10.0 2022-07-13T02:00,20.0,10.0 2022-07-13T03:00,20.0,10.0 2022-07-13T04:00,20.0,10.0 2022-07-13T05:00,20.0,10.0 2022-07-13T06:00,20.0,10.0 2022-07-13T07:00,20.0,10.0 2022-07-13T08:00,20.0,10.0 2022-07-13T09:00,20.0,10.0 2022-07-13T10:00,20.0,10.0 2022-07-13T11:00,20.0,10.0 2022-07-13T12:00,20.0,10.0 2022-07-13T13:00,20.0,10.0 2022-07-13T14:00,20.0,10.0 2022-07-13T15:00,20.0,10.0 2022-07-13T16:00,20.0,10.0 2022-07-13T17:00,20.0,10.0 2022-07-13T18:00,20.0,10.0 2022-07-13T19:00,20.0,10.0 2022-07-13T20:00,20.0,10.0 2022-07-13T21:00,20.0,10.0 2022-07-13T22:00,20.0,10.0 2022-07-13T23:00,20.0,10.0 2022-07-14T00:00,20.0,10.0 time,temperature_2m_mean (°C),windspeed_10m_mean (km/h) 2022-07-12,20.0,10.0 2022-07-13,20.0,10.0 """) let csvUnix = await drainString(try dataUnix.response(format: .csv, numberOfLocationsMaximum: numberOfLocationsMaximum)) XCTAssertEqual(csvUnix, """ latitude,longitude,elevation,utc_offset_seconds,timezone,timezone_abbreviation 41.0,2.0,NaN,3600,GMT,GMT time,temperature_20m (°C),windspeed_100m (km/h) 1657588500,20.0,10.0 time,temperature_2m (°C),windspeed_10m (km/h) 1657584000,20.0,10.0 1657587600,20.0,10.0 1657591200,20.0,10.0 1657594800,20.0,10.0 1657598400,20.0,10.0 1657602000,20.0,10.0 1657605600,20.0,10.0 1657609200,20.0,10.0 1657612800,20.0,10.0 1657616400,20.0,10.0 1657620000,20.0,10.0 1657623600,20.0,10.0 1657627200,20.0,10.0 1657630800,20.0,10.0 1657634400,20.0,10.0 1657638000,20.0,10.0 1657641600,20.0,10.0 1657645200,20.0,10.0 1657648800,20.0,10.0 1657652400,20.0,10.0 1657656000,20.0,10.0 1657659600,20.0,10.0 1657663200,20.0,10.0 1657666800,20.0,10.0 1657670400,20.0,10.0 1657674000,20.0,10.0 1657677600,20.0,10.0 1657681200,20.0,10.0 1657684800,20.0,10.0 1657688400,20.0,10.0 1657692000,20.0,10.0 1657695600,20.0,10.0 1657699200,20.0,10.0 1657702800,20.0,10.0 1657706400,20.0,10.0 1657710000,20.0,10.0 1657713600,20.0,10.0 1657717200,20.0,10.0 1657720800,20.0,10.0 1657724400,20.0,10.0 1657728000,20.0,10.0 1657731600,20.0,10.0 1657735200,20.0,10.0 1657738800,20.0,10.0 1657742400,20.0,10.0 1657746000,20.0,10.0 1657749600,20.0,10.0 1657753200,20.0,10.0 time,temperature_2m_mean (°C),windspeed_10m_mean (km/h) 1657584000,20.0,10.0 1657670400,20.0,10.0 """) /// needs to set a timestamp, because of zip compression headers let xlsx = await drainData(try data.response(format: .xlsx, numberOfLocationsMaximum: numberOfLocationsMaximum, timestamp: Timestamp(2022,7,13))).sha256 XCTAssertEqual(xlsx, "fe097d32e320d1d122a1f391400e8cdb718d41c23bab8b976fdf8ad3db491024") let flatbuffers = await drainData(try data.response(format: .flatbuffers, numberOfLocationsMaximum: numberOfLocationsMaximum, fixedGenerationTime: 12)).sha256 XCTAssertEqual(flatbuffers, "a1dadac11cfff2adbc09ff15355c9fa87f2671f16477d77312ef60e916a7c683") } /// Test output formats for 2 locations func testFormatsMultiLocation() async throws { /*let current = ForecastapiResult.CurrentWeather( temperature: 23, windspeed: 12, winddirection: 90, weathercode: 5, is_day: 1, temperature_unit: .celsius, windspeed_unit: .kmh, winddirection_unit: .degreeDirection, weathercode_unit: .wmoCode, time: Timestamp(2022,7,13,15,0))*/ let daily = ApiSection(name: "daily", time: TimerangeDt(start: Timestamp(2022,7,12,0), nTime: 2, dtSeconds: 86400), columns: [ ApiColumn(variable: .temperature_2m_mean, unit: .celsius, variables: [.float(.init(repeating: 20, count: 2))]), ApiColumn(variable: .windspeed_10m_mean, unit: .kilometresPerHour, variables: [.float(.init(repeating: 10, count: 2))]), ]) let hourly = ApiSection.SurfacePressureAndHeightVariable>(name: "hourly", time: TimerangeDt(start: Timestamp(2022,7,12,0), nTime: 48, dtSeconds: 3600), columns: [ ApiColumn(variable: .surface(.init(.temperature_2m, 0)), unit: .celsius, variables: [.float(.init(repeating: 20, count: 48))]), ApiColumn(variable: .surface(.init(.windspeed_10m, 0)), unit: .kilometresPerHour, variables: [.float(.init(repeating: 10, count: 48))]), ]) let currentSection = ApiSectionSingle.SurfacePressureAndHeightVariable>(name: "current_weather", time: Timestamp(2022,7,12,1,15), dtSeconds: 3600/4, columns: [ ApiColumnSingle(variable: .surface(.init(.temperature_20m, 0)), unit: .celsius, value: 20), ApiColumnSingle(variable: .surface(.init(.windspeed_100m, 0)), unit: .kilometresPerHour, value: 10), ]) let numberOfLocationsMaximum: (numberOfLocations: Int, apikey: String?) = (1000, nil) let res = ForecastapiResult.PerModel( model: .best_match, latitude: 41, longitude: 2, elevation: nil, prefetch: {}, current: {currentSection}, hourly: { hourly }, daily: { daily }, sixHourly: nil, minutely15: nil ) let location1 = ForecastapiResult.PerLocation(timezone: .init(utcOffsetSeconds: 3600, identifier: "GMT", abbreviation: "GMT"), time: TimerangeLocal(range: daily.time.range, utcOffsetSeconds: 0), locationId: 0, results: [res]) let location2 = ForecastapiResult.PerLocation(timezone: .init(utcOffsetSeconds: 3600, identifier: "GMT", abbreviation: "GMT"), time: TimerangeLocal(range: daily.time.range, utcOffsetSeconds: 0), locationId: 1, results: [res]) let data = ForecastapiResult(timeformat: .iso8601, results: [location1, location2]) XCTAssertEqual(data.calculateQueryWeight(nVariablesModels: 2), 2) XCTAssertEqual(data.calculateQueryWeight(nVariablesModels: 15), 3) XCTAssertEqual(data.calculateQueryWeight(nVariablesModels: 20), 4) let json = await drainString(try data.response(format: .json, numberOfLocationsMaximum: numberOfLocationsMaximum, fixedGenerationTime: 12)) XCTAssertEqual(json, """ [{"latitude":41.0,"longitude":2.0,"generationtime_ms":12.0,"utc_offset_seconds":3600,"timezone":"GMT","timezone_abbreviation":"GMT","current_weather_units":{"time":"iso8601","interval":"seconds","temperature_20m":"°C","windspeed_100m":"km/h"},"current_weather":{"time":"2022-07-12T02:15","interval":900,"temperature_20m":20.0,"windspeed_100m":10.0},"hourly_units":{"time":"iso8601","temperature_2m":"°C","windspeed_10m":"km/h"},"hourly":{"time":["2022-07-12T01:00","2022-07-12T02:00","2022-07-12T03:00","2022-07-12T04:00","2022-07-12T05:00","2022-07-12T06:00","2022-07-12T07:00","2022-07-12T08:00","2022-07-12T09:00","2022-07-12T10:00","2022-07-12T11:00","2022-07-12T12:00","2022-07-12T13:00","2022-07-12T14:00","2022-07-12T15:00","2022-07-12T16:00","2022-07-12T17:00","2022-07-12T18:00","2022-07-12T19:00","2022-07-12T20:00","2022-07-12T21:00","2022-07-12T22:00","2022-07-12T23:00","2022-07-13T00:00","2022-07-13T01:00","2022-07-13T02:00","2022-07-13T03:00","2022-07-13T04:00","2022-07-13T05:00","2022-07-13T06:00","2022-07-13T07:00","2022-07-13T08:00","2022-07-13T09:00","2022-07-13T10:00","2022-07-13T11:00","2022-07-13T12:00","2022-07-13T13:00","2022-07-13T14:00","2022-07-13T15:00","2022-07-13T16:00","2022-07-13T17:00","2022-07-13T18:00","2022-07-13T19:00","2022-07-13T20:00","2022-07-13T21:00","2022-07-13T22:00","2022-07-13T23:00","2022-07-14T00:00"],"temperature_2m":[20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0],"windspeed_10m":[10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0]},"daily_units":{"time":"iso8601","temperature_2m_mean":"°C","windspeed_10m_mean":"km/h"},"daily":{"time":["2022-07-12","2022-07-13"],"temperature_2m_mean":[20.0,20.0],"windspeed_10m_mean":[10.0,10.0]}},{"latitude":41.0,"longitude":2.0,"generationtime_ms":12.0,"utc_offset_seconds":3600,"timezone":"GMT","timezone_abbreviation":"GMT","location_id":1,"current_weather_units":{"time":"iso8601","interval":"seconds","temperature_20m":"°C","windspeed_100m":"km/h"},"current_weather":{"time":"2022-07-12T02:15","interval":900,"temperature_20m":20.0,"windspeed_100m":10.0},"hourly_units":{"time":"iso8601","temperature_2m":"°C","windspeed_10m":"km/h"},"hourly":{"time":["2022-07-12T01:00","2022-07-12T02:00","2022-07-12T03:00","2022-07-12T04:00","2022-07-12T05:00","2022-07-12T06:00","2022-07-12T07:00","2022-07-12T08:00","2022-07-12T09:00","2022-07-12T10:00","2022-07-12T11:00","2022-07-12T12:00","2022-07-12T13:00","2022-07-12T14:00","2022-07-12T15:00","2022-07-12T16:00","2022-07-12T17:00","2022-07-12T18:00","2022-07-12T19:00","2022-07-12T20:00","2022-07-12T21:00","2022-07-12T22:00","2022-07-12T23:00","2022-07-13T00:00","2022-07-13T01:00","2022-07-13T02:00","2022-07-13T03:00","2022-07-13T04:00","2022-07-13T05:00","2022-07-13T06:00","2022-07-13T07:00","2022-07-13T08:00","2022-07-13T09:00","2022-07-13T10:00","2022-07-13T11:00","2022-07-13T12:00","2022-07-13T13:00","2022-07-13T14:00","2022-07-13T15:00","2022-07-13T16:00","2022-07-13T17:00","2022-07-13T18:00","2022-07-13T19:00","2022-07-13T20:00","2022-07-13T21:00","2022-07-13T22:00","2022-07-13T23:00","2022-07-14T00:00"],"temperature_2m":[20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0],"windspeed_10m":[10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0]},"daily_units":{"time":"iso8601","temperature_2m_mean":"°C","windspeed_10m_mean":"km/h"},"daily":{"time":["2022-07-12","2022-07-13"],"temperature_2m_mean":[20.0,20.0],"windspeed_10m_mean":[10.0,10.0]}}] """) let dataUnix = ForecastapiResult(timeformat: .unixtime, results: [location1, location2]) let jsonUnix = await drainString(try dataUnix.response(format: .json, numberOfLocationsMaximum: numberOfLocationsMaximum, fixedGenerationTime: 12)) XCTAssertEqual(jsonUnix, """ [{"latitude":41.0,"longitude":2.0,"generationtime_ms":12.0,"utc_offset_seconds":3600,"timezone":"GMT","timezone_abbreviation":"GMT","current_weather_units":{"time":"unixtime","interval":"seconds","temperature_20m":"°C","windspeed_100m":"km/h"},"current_weather":{"time":1657588500,"interval":900,"temperature_20m":20.0,"windspeed_100m":10.0},"hourly_units":{"time":"unixtime","temperature_2m":"°C","windspeed_10m":"km/h"},"hourly":{"time":[1657584000,1657587600,1657591200,1657594800,1657598400,1657602000,1657605600,1657609200,1657612800,1657616400,1657620000,1657623600,1657627200,1657630800,1657634400,1657638000,1657641600,1657645200,1657648800,1657652400,1657656000,1657659600,1657663200,1657666800,1657670400,1657674000,1657677600,1657681200,1657684800,1657688400,1657692000,1657695600,1657699200,1657702800,1657706400,1657710000,1657713600,1657717200,1657720800,1657724400,1657728000,1657731600,1657735200,1657738800,1657742400,1657746000,1657749600,1657753200],"temperature_2m":[20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0],"windspeed_10m":[10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0]},"daily_units":{"time":"unixtime","temperature_2m_mean":"°C","windspeed_10m_mean":"km/h"},"daily":{"time":[1657584000,1657670400],"temperature_2m_mean":[20.0,20.0],"windspeed_10m_mean":[10.0,10.0]}},{"latitude":41.0,"longitude":2.0,"generationtime_ms":12.0,"utc_offset_seconds":3600,"timezone":"GMT","timezone_abbreviation":"GMT","location_id":1,"current_weather_units":{"time":"unixtime","interval":"seconds","temperature_20m":"°C","windspeed_100m":"km/h"},"current_weather":{"time":1657588500,"interval":900,"temperature_20m":20.0,"windspeed_100m":10.0},"hourly_units":{"time":"unixtime","temperature_2m":"°C","windspeed_10m":"km/h"},"hourly":{"time":[1657584000,1657587600,1657591200,1657594800,1657598400,1657602000,1657605600,1657609200,1657612800,1657616400,1657620000,1657623600,1657627200,1657630800,1657634400,1657638000,1657641600,1657645200,1657648800,1657652400,1657656000,1657659600,1657663200,1657666800,1657670400,1657674000,1657677600,1657681200,1657684800,1657688400,1657692000,1657695600,1657699200,1657702800,1657706400,1657710000,1657713600,1657717200,1657720800,1657724400,1657728000,1657731600,1657735200,1657738800,1657742400,1657746000,1657749600,1657753200],"temperature_2m":[20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0,20.0],"windspeed_10m":[10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0]},"daily_units":{"time":"unixtime","temperature_2m_mean":"°C","windspeed_10m_mean":"km/h"},"daily":{"time":[1657584000,1657670400],"temperature_2m_mean":[20.0,20.0],"windspeed_10m_mean":[10.0,10.0]}}] """) let csv = await drainString(try data.response(format: .csv, numberOfLocationsMaximum: numberOfLocationsMaximum)) XCTAssertEqual(csv, """ location_id,latitude,longitude,elevation,utc_offset_seconds,timezone,timezone_abbreviation 0,41.0,2.0,NaN,3600,GMT,GMT 1,41.0,2.0,NaN,3600,GMT,GMT location_id,time,temperature_20m (°C),windspeed_100m (km/h) 0,2022-07-12T02:15,20.0,10.0 1,2022-07-12T02:15,20.0,10.0 location_id,time,temperature_2m (°C),windspeed_10m (km/h) 0,2022-07-12T01:00,20.0,10.0 0,2022-07-12T02:00,20.0,10.0 0,2022-07-12T03:00,20.0,10.0 0,2022-07-12T04:00,20.0,10.0 0,2022-07-12T05:00,20.0,10.0 0,2022-07-12T06:00,20.0,10.0 0,2022-07-12T07:00,20.0,10.0 0,2022-07-12T08:00,20.0,10.0 0,2022-07-12T09:00,20.0,10.0 0,2022-07-12T10:00,20.0,10.0 0,2022-07-12T11:00,20.0,10.0 0,2022-07-12T12:00,20.0,10.0 0,2022-07-12T13:00,20.0,10.0 0,2022-07-12T14:00,20.0,10.0 0,2022-07-12T15:00,20.0,10.0 0,2022-07-12T16:00,20.0,10.0 0,2022-07-12T17:00,20.0,10.0 0,2022-07-12T18:00,20.0,10.0 0,2022-07-12T19:00,20.0,10.0 0,2022-07-12T20:00,20.0,10.0 0,2022-07-12T21:00,20.0,10.0 0,2022-07-12T22:00,20.0,10.0 0,2022-07-12T23:00,20.0,10.0 0,2022-07-13T00:00,20.0,10.0 0,2022-07-13T01:00,20.0,10.0 0,2022-07-13T02:00,20.0,10.0 0,2022-07-13T03:00,20.0,10.0 0,2022-07-13T04:00,20.0,10.0 0,2022-07-13T05:00,20.0,10.0 0,2022-07-13T06:00,20.0,10.0 0,2022-07-13T07:00,20.0,10.0 0,2022-07-13T08:00,20.0,10.0 0,2022-07-13T09:00,20.0,10.0 0,2022-07-13T10:00,20.0,10.0 0,2022-07-13T11:00,20.0,10.0 0,2022-07-13T12:00,20.0,10.0 0,2022-07-13T13:00,20.0,10.0 0,2022-07-13T14:00,20.0,10.0 0,2022-07-13T15:00,20.0,10.0 0,2022-07-13T16:00,20.0,10.0 0,2022-07-13T17:00,20.0,10.0 0,2022-07-13T18:00,20.0,10.0 0,2022-07-13T19:00,20.0,10.0 0,2022-07-13T20:00,20.0,10.0 0,2022-07-13T21:00,20.0,10.0 0,2022-07-13T22:00,20.0,10.0 0,2022-07-13T23:00,20.0,10.0 0,2022-07-14T00:00,20.0,10.0 1,2022-07-12T01:00,20.0,10.0 1,2022-07-12T02:00,20.0,10.0 1,2022-07-12T03:00,20.0,10.0 1,2022-07-12T04:00,20.0,10.0 1,2022-07-12T05:00,20.0,10.0 1,2022-07-12T06:00,20.0,10.0 1,2022-07-12T07:00,20.0,10.0 1,2022-07-12T08:00,20.0,10.0 1,2022-07-12T09:00,20.0,10.0 1,2022-07-12T10:00,20.0,10.0 1,2022-07-12T11:00,20.0,10.0 1,2022-07-12T12:00,20.0,10.0 1,2022-07-12T13:00,20.0,10.0 1,2022-07-12T14:00,20.0,10.0 1,2022-07-12T15:00,20.0,10.0 1,2022-07-12T16:00,20.0,10.0 1,2022-07-12T17:00,20.0,10.0 1,2022-07-12T18:00,20.0,10.0 1,2022-07-12T19:00,20.0,10.0 1,2022-07-12T20:00,20.0,10.0 1,2022-07-12T21:00,20.0,10.0 1,2022-07-12T22:00,20.0,10.0 1,2022-07-12T23:00,20.0,10.0 1,2022-07-13T00:00,20.0,10.0 1,2022-07-13T01:00,20.0,10.0 1,2022-07-13T02:00,20.0,10.0 1,2022-07-13T03:00,20.0,10.0 1,2022-07-13T04:00,20.0,10.0 1,2022-07-13T05:00,20.0,10.0 1,2022-07-13T06:00,20.0,10.0 1,2022-07-13T07:00,20.0,10.0 1,2022-07-13T08:00,20.0,10.0 1,2022-07-13T09:00,20.0,10.0 1,2022-07-13T10:00,20.0,10.0 1,2022-07-13T11:00,20.0,10.0 1,2022-07-13T12:00,20.0,10.0 1,2022-07-13T13:00,20.0,10.0 1,2022-07-13T14:00,20.0,10.0 1,2022-07-13T15:00,20.0,10.0 1,2022-07-13T16:00,20.0,10.0 1,2022-07-13T17:00,20.0,10.0 1,2022-07-13T18:00,20.0,10.0 1,2022-07-13T19:00,20.0,10.0 1,2022-07-13T20:00,20.0,10.0 1,2022-07-13T21:00,20.0,10.0 1,2022-07-13T22:00,20.0,10.0 1,2022-07-13T23:00,20.0,10.0 1,2022-07-14T00:00,20.0,10.0 location_id,time,temperature_2m_mean (°C),windspeed_10m_mean (km/h) 0,2022-07-12,20.0,10.0 0,2022-07-13,20.0,10.0 1,2022-07-12,20.0,10.0 1,2022-07-13,20.0,10.0 """) let csvUnix = await drainString(try dataUnix.response(format: .csv, numberOfLocationsMaximum: numberOfLocationsMaximum)) XCTAssertEqual(csvUnix, """ location_id,latitude,longitude,elevation,utc_offset_seconds,timezone,timezone_abbreviation 0,41.0,2.0,NaN,3600,GMT,GMT 1,41.0,2.0,NaN,3600,GMT,GMT location_id,time,temperature_20m (°C),windspeed_100m (km/h) 0,1657588500,20.0,10.0 1,1657588500,20.0,10.0 location_id,time,temperature_2m (°C),windspeed_10m (km/h) 0,1657584000,20.0,10.0 0,1657587600,20.0,10.0 0,1657591200,20.0,10.0 0,1657594800,20.0,10.0 0,1657598400,20.0,10.0 0,1657602000,20.0,10.0 0,1657605600,20.0,10.0 0,1657609200,20.0,10.0 0,1657612800,20.0,10.0 0,1657616400,20.0,10.0 0,1657620000,20.0,10.0 0,1657623600,20.0,10.0 0,1657627200,20.0,10.0 0,1657630800,20.0,10.0 0,1657634400,20.0,10.0 0,1657638000,20.0,10.0 0,1657641600,20.0,10.0 0,1657645200,20.0,10.0 0,1657648800,20.0,10.0 0,1657652400,20.0,10.0 0,1657656000,20.0,10.0 0,1657659600,20.0,10.0 0,1657663200,20.0,10.0 0,1657666800,20.0,10.0 0,1657670400,20.0,10.0 0,1657674000,20.0,10.0 0,1657677600,20.0,10.0 0,1657681200,20.0,10.0 0,1657684800,20.0,10.0 0,1657688400,20.0,10.0 0,1657692000,20.0,10.0 0,1657695600,20.0,10.0 0,1657699200,20.0,10.0 0,1657702800,20.0,10.0 0,1657706400,20.0,10.0 0,1657710000,20.0,10.0 0,1657713600,20.0,10.0 0,1657717200,20.0,10.0 0,1657720800,20.0,10.0 0,1657724400,20.0,10.0 0,1657728000,20.0,10.0 0,1657731600,20.0,10.0 0,1657735200,20.0,10.0 0,1657738800,20.0,10.0 0,1657742400,20.0,10.0 0,1657746000,20.0,10.0 0,1657749600,20.0,10.0 0,1657753200,20.0,10.0 1,1657584000,20.0,10.0 1,1657587600,20.0,10.0 1,1657591200,20.0,10.0 1,1657594800,20.0,10.0 1,1657598400,20.0,10.0 1,1657602000,20.0,10.0 1,1657605600,20.0,10.0 1,1657609200,20.0,10.0 1,1657612800,20.0,10.0 1,1657616400,20.0,10.0 1,1657620000,20.0,10.0 1,1657623600,20.0,10.0 1,1657627200,20.0,10.0 1,1657630800,20.0,10.0 1,1657634400,20.0,10.0 1,1657638000,20.0,10.0 1,1657641600,20.0,10.0 1,1657645200,20.0,10.0 1,1657648800,20.0,10.0 1,1657652400,20.0,10.0 1,1657656000,20.0,10.0 1,1657659600,20.0,10.0 1,1657663200,20.0,10.0 1,1657666800,20.0,10.0 1,1657670400,20.0,10.0 1,1657674000,20.0,10.0 1,1657677600,20.0,10.0 1,1657681200,20.0,10.0 1,1657684800,20.0,10.0 1,1657688400,20.0,10.0 1,1657692000,20.0,10.0 1,1657695600,20.0,10.0 1,1657699200,20.0,10.0 1,1657702800,20.0,10.0 1,1657706400,20.0,10.0 1,1657710000,20.0,10.0 1,1657713600,20.0,10.0 1,1657717200,20.0,10.0 1,1657720800,20.0,10.0 1,1657724400,20.0,10.0 1,1657728000,20.0,10.0 1,1657731600,20.0,10.0 1,1657735200,20.0,10.0 1,1657738800,20.0,10.0 1,1657742400,20.0,10.0 1,1657746000,20.0,10.0 1,1657749600,20.0,10.0 1,1657753200,20.0,10.0 location_id,time,temperature_2m_mean (°C),windspeed_10m_mean (km/h) 0,1657584000,20.0,10.0 0,1657670400,20.0,10.0 1,1657584000,20.0,10.0 1,1657670400,20.0,10.0 """) /// needs to set a timestamp, because of zip compression headers let xlsx = await drainData(try data.response(format: .xlsx, numberOfLocationsMaximum: numberOfLocationsMaximum, timestamp: Timestamp(2022,7,13))).sha256 XCTAssertEqual(xlsx, "91e1b562e1a7ef1fafe7894f0a797162405eb30677099a5f8e9665d7b180026b") let flatbuffers = await drainData(try data.response(format: .flatbuffers, numberOfLocationsMaximum: numberOfLocationsMaximum, fixedGenerationTime: 12)).sha256 XCTAssertEqual(flatbuffers, "52899e668476fef0cbc11934eedcd8adafd54e2f413fef3e7774abce1c62f73b") } func testXlsxWriter() throws { let xlsx = try XlsxWriter() xlsx.startRow() xlsx.write(5) xlsx.writeTimestamp(Timestamp(2022,07,10,5,6)) xlsx.write(42.1, significantDigits: 1) xlsx.endRow() var data = xlsx.write(timestamp: Timestamp(2022,7,10)) //try data.write(to: URL(fileURLWithPath: "/Users/patrick/Downloads/test.xlsx")) XCTAssertEqual(data.readData(length: data.writerIndex)!.sha256, "987fff4d1b6ba45e799e204c55ca03a53794e6479c5c497c0c4fa279f0f6c0f6") } func testGzipStream() throws { let hello = try GzipStream(level: 6, chunkCapacity: 512) hello.write("Hello") let world = try GzipStream(level: 6, chunkCapacity: 512) world.write("World") let helloGz = hello.finish() let worldGz = world.finish() var zip = ZipWriter.zip(files: [("hello.txt", helloGz), ("world.txt", worldGz)], timestamp: Timestamp(2000,1,1)) XCTAssertEqual(zip.readData(length: zip.writerIndex)!.sha256, "443f2602754152053754ff14b49218858bd555e74b5d8dc8d5e16fc85c7cdcce") } }