add all api to sort by month

This commit is contained in:
Michael Simard
2020-06-14 21:46:44 -05:00
parent 6b9e9a4c35
commit 842abb0895
162 changed files with 34480 additions and 71 deletions

View File

@@ -0,0 +1,932 @@
//
// SwiftDate
// Parse, validate, manipulate, and display dates, time and timezones in Swift
//
// Created by Daniele Margutti
// - Web: https://www.danielemargutti.com
// - Twitter: https://twitter.com/danielemargutti
// - Mail: hello@danielemargutti.com
//
// Copyright © 2019 Daniele Margutti. Licensed under MIT License.
//
// swiftlint:disable file_length
import Foundation
/// This defines all possible errors you can encounter parsing ISO8601 string
///
/// - eof: end of file
/// - notDigit: expected digit, value cannot be parsed as int
/// - notDouble: expected double digit, value cannot be parsed as double
/// - invalid: invalid state reached. Something in the format is not correct
public enum ISO8601ParserError: Error {
case eof
case notDigit
case notDouble
case invalid
}
fileprivate extension Int {
/// Return `true` if current year is a leap year, `false` otherwise
var isLeapYear: Bool {
return ((self % 4) == 0) && (((self % 100) != 0) || ((self % 400) == 0))
}
}
// MARK: - Internal Extension for UnicodeScalar type
internal extension UnicodeScalar {
/// return `true` if current character is a digit (arabic), `false` otherwise
var isDigit: Bool {
return "0"..."9" ~= self
}
/// return `true` if current character is a space
var isSpace: Bool {
return CharacterSet.whitespaces.contains(self)
}
}
/// This is the ISO8601 Parser class: it evaluates automatically the format of the ISO8601 date
/// and attempt to parse it in a valid `Date` object.
/// Resulting date also includes Time Zone settings and a property which allows you to inspect
/// single date components.
///
/// This work is inspired to the original ISO8601DateFormatter class written in ObjC by
/// Peter Hosey (available here https://bitbucket.org/boredzo/iso-8601-parser-unparser).
/// I've made a Swift porting and fixed some issues when parsing several ISO8601 date variants.
// swiftlint:disable type_body_length
public class ISOParser: StringToDateTransformable {
/// Internal structure
internal enum Weekday: Int {
case monday = 0
case tuesday = 1
case wednesday = 2
case thursday = 3
}
public struct Options {
/// Time separator character. By default is `:`.
var time_separator: ISOParser.ISOChar = ":"
/// Strict parsing. By default is `false`.
var strict: Bool = false
public init(strict: Bool = false) {
self.strict = strict
}
}
/// Some typealias to make the code cleaner
public typealias ISOString = String.UnicodeScalarView
public typealias ISOIndex = String.UnicodeScalarView.Index
public typealias ISOChar = UnicodeScalar
public typealias ISOParsedDate = (date: Date?, timezone: TimeZone?)
/// This represent the internal parser status representation
public struct ParsedDate {
/// Type of date parsed
///
/// - monthAndDate: month and date style
/// - week: date with week number
/// - dateOnly: date only
// swiftlint:disable nesting
public enum DateStyle {
case monthAndDate
case week
case dateOnly
}
/// Parsed year value
var year: Int = 0
/// Parsed month or week number
var month_or_week: Int = 0
/// Parsed day value
var day: Int = 0
/// Parsed hour value
var hour: Int = 0
/// Parsed minutes value
var minute: TimeInterval = 0.0
/// Parsed seconds value
var seconds: TimeInterval = 0.0
/// Parsed nanoseconds value
var nanoseconds: TimeInterval = 0.0
/// Parsed weekday number (1=monday, 7=sunday)
/// If `nil` source string has not specs about weekday.
var weekday: Int?
/// Timezone parsed hour value
var tz_hour: Int = 0
/// Timezone parsed minute value
var tz_minute: Int = 0
/// Type of parsed date
var type: DateStyle = .monthAndDate
/// Parsed timezone object
var timezone: TimeZone?
}
/// Source generation calendar.
private var srcCalendar = Calendars.gregorian.toCalendar()
/// Source raw parsed values
private var date = ParsedDate()
/// Source string represented as unicode scalars
private var string: ISOString
/// Current position of the parser in source string.
/// Initially is equal to `string.startIndex`
private var cIdx: ISOIndex
/// Just a shortcut to the last index in source string
private var eIdx: ISOIndex
/// Lenght of the string
private var length: Int
/// Number of hyphens characters found before any value
/// Consequential "-" are used to define implicit values in dates.
private var hyphens: Int = 0
/// Private date components used for default values
private var now_cmps: DateComponents
/// Configuration used for parser
private var options: ISOParser.Options
/// Date components parsed
private(set) var date_components: DateComponents?
/// Parsed date
private(set) var parsedDate: Date?
/// Parsed timezone
private(set) var parsedTimeZone: TimeZone?
/// Date adjusted at parsed timezone
private var dateInTimezone: Date? {
get {
srcCalendar.timeZone = date.timezone ?? TimeZone(identifier: "UTC")!
return srcCalendar.date(from: date_components!)
}
}
/// Initialize a new parser with a source ISO8601 string to parse
/// Parsing is done during initialization; any exception is reported
/// before allocating.
///
/// - Parameters:
/// - src: source ISO8601 string
/// - config: configuration used for parsing
/// - Throws: throw an `ISO8601Error` if parsing operation fails
public init?(_ src: String, options: ISOParser.Options? = nil) {
let src_trimmed = src.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
guard src_trimmed.count > 0 else {
return nil
}
string = src_trimmed.unicodeScalars
length = src_trimmed.count
cIdx = string.startIndex
eIdx = string.endIndex
self.options = (options ?? ISOParser.Options())
self.now_cmps = srcCalendar.dateComponents([.year, .month, .day], from: Date())
var idx = cIdx
while idx < eIdx {
if string[idx] == "-" { hyphens += 1 } else { break }
idx = string.index(after: idx)
}
do {
try parse()
} catch {
return nil
}
}
// MARK: - Internal Parser
/// Private parsing function
///
/// - Throws: throw an `ISO8601Error` if parsing operation fails
@discardableResult
private func parse() throws -> ISOParsedDate {
// PARSE DATE
if current() == "T" {
// There is no date here, only a time.
// Set the date to now; then we'll parse the time.
next()
guard current()?.isDigit ?? false else {
throw ISO8601ParserError.invalid
}
date.year = now_cmps.year!
date.month_or_week = now_cmps.month!
date.day = now_cmps.day!
} else {
moveUntil(is: "-")
let is_time_only = (string.contains("T") == false && string.contains(":") && !string.contains("-"))
if is_time_only == false {
var (num_digits, segment) = try read_int()
switch num_digits {
case 0: try parse_digits_0(num_digits, &segment)
case 8: try parse_digits_8(num_digits, &segment)
case 6: try parse_digits_6(num_digits, &segment)
case 4: try parse_digits_4(num_digits, &segment)
case 5: try parse_digits_5(num_digits, &segment)
case 1: try parse_digits_1(num_digits, &segment)
case 2: try parse_digits_2(num_digits, &segment)
case 7: try parse_digits_7(num_digits, &segment) //YYYY DDD (ordinal date)
case 3: try parse_digits_3(num_digits, &segment) //--DDD (ordinal date, implicit year)
default: throw ISO8601ParserError.invalid
}
} else {
date.year = now_cmps.year!
date.month_or_week = now_cmps.month!
date.day = now_cmps.day!
}
}
var hasTime = false
if current()?.isSpace ?? false || current() == "T" {
hasTime = true
next()
}
// PARSE TIME
if current()?.isDigit ?? false == true {
let time_sep = options.time_separator
let hasTimeSeparator = string.contains(time_sep)
date.hour = try read_int(2).value
if hasTimeSeparator == false && hasTime {
date.minute = TimeInterval(try read_int(2).value)
} else if current() == time_sep {
next()
if time_sep == "," || time_sep == "." {
//We can't do fractional minutes when '.' is the segment separator.
//Only allow whole minutes and whole seconds.
date.minute = TimeInterval(try read_int(2).value)
if current() == time_sep {
next()
date.seconds = TimeInterval(try read_int(2).value)
}
} else {
//Allow a fractional minute.
//If we don't get a fraction, look for a seconds segment.
//Otherwise, the fraction of a minute is the seconds.
date.minute = try read_double().value
if current() != ":" {
var int_part: Double = 0.0
var frac_part: Double = 0.0
frac_part = modf(date.minute, &int_part)
date.minute = int_part
date.seconds = frac_part
if date.seconds > Double.ulpOfOne {
// Convert fraction (e.g. .5) into seconds (e.g. 30).
date.seconds *= 60
} else if current() == time_sep {
next()
// date.seconds = try read_double().value
let value = try modf(read_double().value)
date.nanoseconds = TimeInterval(round(value.1 * 1000) * 1_000_000)
date.seconds = TimeInterval(value.0)
}
} else {
// fractional minutes
next()
let value = try modf(read_double().value)
date.nanoseconds = TimeInterval(round(value.1 * 1000) * 1_000_000)
date.seconds = TimeInterval(value.0)
}
}
}
if options.strict == false {
if cIdx != eIdx && current()?.isSpace ?? false == true {
next()
}
}
if cIdx != eIdx {
switch current() {
case "Z":
date.timezone = TimeZone(abbreviation: "UTC")
case "+", "-":
let is_negative = current() == "-"
next()
if current()?.isDigit ?? false == true {
//Read hour offset.
date.tz_hour = try read_int(2).value
if is_negative == true { date.tz_hour = -date.tz_hour }
// Optional separator
if current() == time_sep {
next()
}
if current()?.isDigit ?? false {
// Read minute offset
date.tz_minute = try read_int(2).value
if is_negative == true { date.tz_minute = -date.tz_minute }
}
let timezone_offset = (date.tz_hour * 3600) + (date.tz_minute * 60)
date.timezone = TimeZone(secondsFromGMT: timezone_offset)
}
default:
break
}
}
}
date_components = DateComponents()
date_components!.year = date.year
date_components!.day = date.day
date_components!.hour = date.hour
date_components!.minute = Int(date.minute)
date_components!.second = Int(date.seconds)
date_components!.nanosecond = Int(date.nanoseconds)
switch date.type {
case .monthAndDate:
date_components!.month = date.month_or_week
case .week:
//Adapted from <http://personal.ecu.edu/mccartyr/ISOwdALG.txt>.
//This works by converting the week date into an ordinal date, then letting the next case handle it.
let prevYear = date.year - 1
let YY = prevYear % 100
let prevC = prevYear - YY
let prevG = YY + YY / 4
let isLeapYear = (((prevC / 100) % 4) * 5)
let jan1Weekday = ((isLeapYear + prevG) % 7)
var day = ((8 - jan1Weekday) + (7 * (jan1Weekday > Weekday.thursday.rawValue ? 1 : 0)))
day += (date.day - 1) + (7 * (date.month_or_week - 2))
if let weekday = date.weekday {
//date_components!.weekday = weekday
date_components!.day = day + weekday
} else {
date_components!.day = day
}
case .dateOnly: //An "ordinal date".
break
}
//cfg.calendar.timeZone = date.timezone ?? TimeZone(identifier: "UTC")!
//parsedDate = cfg.calendar.date(from: date_components!)
let tz = date.timezone ?? TimeZone(identifier: "UTC")!
parsedTimeZone = tz
srcCalendar.timeZone = tz
parsedDate = srcCalendar.date(from: date_components!)
return (parsedDate, parsedTimeZone)
}
private func parse_digits_3(_ num_digits: Int, _ segment: inout Int) throws {
//Technically, the standard only allows one hyphen. But it says that two hyphens is the logical implementation, and one was dropped for brevity. So I have chosen to allow the missing hyphen.
if hyphens < 1 || (hyphens > 2 && options.strict == false) {
throw ISO8601ParserError.invalid
}
date.day = segment
date.year = now_cmps.year!
date.type = .dateOnly
if options.strict == true && (date.day > (365 + (date.year.isLeapYear ? 1 : 0))) {
throw ISO8601ParserError.invalid
}
}
private func parse_digits_7(_ num_digits: Int, _ segment: inout Int) throws {
guard hyphens == 0 else { throw ISO8601ParserError.invalid }
date.day = segment % 1000
date.year = segment / 1000
date.type = .dateOnly
if options.strict == true && (date.day > (365 + (date.year.isLeapYear ? 1 : 0))) {
throw ISO8601ParserError.invalid
}
}
private func parse_digits_2(_ num_digits: Int, _ segment: inout Int) throws {
func parse_hyphens_3(_ num_digits: Int, _ segment: inout Int) throws {
date.year = now_cmps.year!
date.month_or_week = now_cmps.month!
date.day = segment
}
func parse_hyphens_2(_ num_digits: Int, _ segment: inout Int) throws {
date.year = now_cmps.year!
date.month_or_week = segment
if current() == "-" {
next()
date.day = try read_int(2).value
} else {
date.day = 1
}
}
func parse_hyphens_1(_ num_digits: Int, _ segment: inout Int) throws {
let current_year = now_cmps.year!
let current_century = (current_year % 100)
date.year = segment + (current_year - current_century)
if num_digits == 1 { // implied decade
date.year += current_century - (current_year % 10)
}
if current() == "-" {
next()
if current() == "W" {
next()
date.type = .week
}
date.month_or_week = try read_int(2).value
if current() == "-" {
next()
if date.type == .week {
// weekday number
let weekday = try read_int().value
if weekday > 7 {
throw ISO8601ParserError.invalid
}
date.weekday = weekday
} else {
date.day = try read_int().value
if date.day == 0 {
date.day = 1
}
if date.month_or_week == 0 {
date.month_or_week = 1
}
}
} else {
date.day = 1
}
} else {
date.month_or_week = 1
date.day = 1
}
}
func parse_hyphens_0(_ num_digits: Int, _ segment: inout Int) throws {
if current() == "-" {
// Implicit century
date.year = now_cmps.year!
date.year -= (date.year % 100)
date.year += segment
next()
if current() == "W" {
try parseWeekAndDay()
} else if current()?.isDigit ?? false == false {
try centuryOnly(&segment)
} else {
// Get month and/or date.
let (v_count, v_seg) = try read_int()
switch v_count {
case 4: // YY-MMDD
date.day = v_seg % 100
date.month_or_week = v_seg / 100
case 1: // YY-M; YY-M-DD (extension)
if options.strict == true {
throw ISO8601ParserError.invalid
}
case 2: // YY-MM; YY-MM-DD
date.month_or_week = v_seg
if current() == "-" {
next()
if current()?.isDigit ?? false == true {
date.day = try read_int(2).value
} else {
date.day = 1
}
} else {
date.day = 1
}
case 3: // Ordinal date
date.day = v_seg
date.type = .dateOnly
default:
break
}
}
} else if current() == "W" {
date.year = now_cmps.year!
date.year -= (date.year % 100)
date.year += segment
try parseWeekAndDay()
} else {
try centuryOnly(&segment)
}
}
switch hyphens {
case 0: try parse_hyphens_0(num_digits, &segment)
case 1: try parse_hyphens_1(num_digits, &segment) //-YY; -YY-MM (implicit century)
case 2: try parse_hyphens_2(num_digits, &segment) //--MM; --MM-DD
case 3: try parse_hyphens_3(num_digits, &segment) //---DD
default: throw ISO8601ParserError.invalid
}
}
private func parse_digits_1(_ num_digits: Int, _ segment: inout Int) throws {
if options.strict == true {
// Two digits only - never just one.
guard hyphens == 1 else { throw ISO8601ParserError.invalid }
if current() == "-" {
next()
}
next()
guard current() == "W" else { throw ISO8601ParserError.invalid }
date.year = now_cmps.year!
date.year -= (date.year % 10)
date.year += segment
} else {
try parse_digits_2(num_digits, &segment)
}
}
private func parse_digits_5(_ num_digits: Int, _ segment: inout Int) throws {
guard hyphens == 0 else { throw ISO8601ParserError.invalid }
// YYDDD
date.year = now_cmps.year!
date.year -= (date.year % 100)
date.year += segment / 1000
date.day = segment % 1000
date.type = .dateOnly
}
private func parse_digits_4(_ num_digits: Int, _ segment: inout Int) throws {
func parse_hyphens_0(_ num_digits: Int, _ segment: inout Int) throws {
date.year = segment
if current() == "-" {
next()
}
if current()?.isDigit ?? false == false {
if current() == "W" {
try parseWeekAndDay()
} else {
date.month_or_week = 1
date.day = 1
}
} else {
let (v_num, v_seg) = try read_int()
switch v_num {
case 4: // MMDD
date.day = v_seg % 100
date.month_or_week = v_seg / 100
case 2: // MM
date.month_or_week = v_seg
if current() == "-" {
next()
}
if current()?.isDigit ?? false == false {
date.day = 1
} else {
date.day = try read_int().value
}
case 3: // DDD
date.day = v_seg % 1000
date.type = .dateOnly
if options.strict == true && (date.day > 365 + (date.year.isLeapYear ? 1 : 0)) {
throw ISO8601ParserError.invalid
}
default:
throw ISO8601ParserError.invalid
}
}
}
func parse_hyphens_1(_ num_digits: Int, _ segment: inout Int) throws {
date.month_or_week = segment % 100
date.year = segment / 100
if current() == "-" {
next()
}
if current()?.isDigit ?? false == false {
date.day = 1
} else {
date.day = try read_int().value
}
}
func parse_hyphens_2(_ num_digits: Int, _ segment: inout Int) throws {
date.day = segment % 100
date.month_or_week = segment / 100
date.year = now_cmps.year!
}
switch hyphens {
case 0: try parse_hyphens_0(num_digits, &segment) // YYYY
case 1: try parse_hyphens_1(num_digits, &segment) // YYMM
case 2: try parse_hyphens_2(num_digits, &segment) // MMDD
default: throw ISO8601ParserError.invalid
}
}
private func parse_digits_6(_ num_digits: Int, _ segment: inout Int) throws {
// YYMMDD (implicit century)
guard hyphens == 0 else {
throw ISO8601ParserError.invalid
}
date.day = segment % 100
segment /= 100
date.month_or_week = segment % 100
date.year = now_cmps.year!
date.year -= (date.year % 100)
date.year += (segment / 100)
}
private func parse_digits_8(_ num_digits: Int, _ segment: inout Int) throws {
// YYYY MM DD
guard hyphens == 0 else {
throw ISO8601ParserError.invalid
}
date.day = segment % 100
segment /= 100
date.month_or_week = segment % 100
date.year = segment / 100
}
private func parse_digits_0(_ num_digits: Int, _ segment: inout Int) throws {
guard current() == "W" else {
throw ISO8601ParserError.invalid
}
if seek(1) == "-" && isDigit(seek(2)) &&
((hyphens == 1 || hyphens == 2) && options.strict == false) {
date.year = now_cmps.year!
date.month_or_week = 1
next(2)
try parseDayAfterWeek()
} else if hyphens == 1 {
date.year = now_cmps.year!
if current() == "W" {
next()
date.month_or_week = try read_int(2).value
date.type = .week
try parseWeekday()
} else {
try parseDayAfterWeek()
}
} else {
throw ISO8601ParserError.invalid
}
}
private func parseWeekday() throws {
if current() == "-" {
next()
}
let weekday = try read_int().value
if weekday > 7 {
throw ISO8601ParserError.invalid
}
date.type = .week
date.weekday = weekday
}
private func parseWeekAndDay() throws {
next()
if current()?.isDigit ?? false == false {
//Not really a week-based date; just a year followed by '-W'.
guard options.strict == false else {
throw ISO8601ParserError.invalid
}
date.month_or_week = 1
date.day = 1
} else {
date.month_or_week = try read_int(2).value
try parseWeekday()
}
}
private func parseDayAfterWeek() throws {
date.day = current()?.isDigit ?? false == true ? try read_int(2).value : 1
date.type = .week
}
private func centuryOnly(_ segment: inout Int) throws {
date.year = segment * 100 + now_cmps.year! % 100
date.month_or_week = 1
date.day = 1
}
/// Return `true` if given character is a char
///
/// - Parameter char: char to evaluate
/// - Returns: `true` if char is a digit, `false` otherwise
private func isDigit(_ char: UnicodeScalar?) -> Bool {
guard let char = char else { return false }
return char.isDigit
}
/// MARK: - Scanner internal functions
/// Get the value at specified offset from current scanner position without
/// moving the current scanner's index.
///
/// - Parameter offset: offset to move
/// - Returns: char at given position, `nil` if not found
@discardableResult
public func seek(_ offset: Int = 1) -> ISOChar? {
let move_idx = string.index(cIdx, offsetBy: offset)
guard move_idx < eIdx else {
return nil
}
return string[move_idx]
}
/// Return the char at the current position of the scanner
///
/// - Parameter next: if `true` return the current char and move to the next position
/// - Returns: the char sat the current position of the scanner
@discardableResult
public func current(_ next: Bool = false) -> ISOChar? {
guard cIdx != eIdx else { return nil }
let current = string[cIdx]
if next == true { cIdx = string.index(after: cIdx) }
return current
}
/// Move by `offset` characters the index of the scanner and return the char at the current
/// position. If EOF is reached `nil` is returned.
///
/// - Parameter offset: offset value (use negative number to move backwards)
/// - Returns: character at the current position.
@discardableResult
private func next(_ offset: Int = 1) -> ISOChar? {
let next = string.index(cIdx, offsetBy: offset)
guard next < eIdx else {
return nil
}
cIdx = next
return string[cIdx]
}
/// Read from the current scanner index and parse the value as Int.
///
/// - Parameter max_count: number of characters to move. If nil scanners continues until a non
/// digit value is encountered.
/// - Returns: parsed value
/// - Throws: throw an exception if parser fails
@discardableResult
private func read_int(_ max_count: Int? = nil) throws -> (count: Int, value: Int) {
var move_idx = cIdx
var count = 0
while move_idx < eIdx {
if let max = max_count, count >= max { break }
if string[move_idx].isDigit == false { break }
count += 1
move_idx = string.index(after: move_idx)
}
let raw_value = String(string[cIdx..<move_idx])
if raw_value == "" {
return (count, 0)
}
guard let value = Int(raw_value) else {
throw ISO8601ParserError.notDigit
}
cIdx = move_idx
return (count, value)
}
/// Read from the current scanner index and parse the value as Double.
/// If parser fails an exception is throw.
/// Unit separator can be `-` or `,`.
///
/// - Returns: double value
/// - Throws: throw an exception if parser fails
@discardableResult
private func read_double() throws -> (count: Int, value: Double) {
var move_idx = cIdx
var count = 0
var fractional_start = false
while move_idx < eIdx {
let char = string[move_idx]
if char == "." || char == "," {
if fractional_start == true { throw ISO8601ParserError.notDouble } else { fractional_start = true }
} else {
if char.isDigit == false { break }
}
count += 1
move_idx = string.index(after: move_idx)
}
let raw_value = String(string[cIdx..<move_idx]).replacingOccurrences(of: ",", with: ".")
if raw_value == "" {
return (count, 0.0)
}
guard let value = Double(raw_value) else {
throw ISO8601ParserError.notDouble
}
cIdx = move_idx
return (count, value)
}
/// Move the current scanner index to the next position until the current char of the scanner
/// is the given `char` value.
///
/// - Parameter char: char
/// - Returns: the number of characters passed
@discardableResult
private func moveUntil(is char: UnicodeScalar) -> Int {
var move_idx = cIdx
var count = 0
while move_idx < eIdx {
guard string[move_idx] == char else { break }
move_idx = string.index(after: move_idx)
count += 1
}
cIdx = move_idx
return count
}
/// Move the current scanner index to the next position until passed `char` value is
/// encountered or `eof` is reached.
///
/// - Parameter char: char
/// - Returns: the number of characters passed
@discardableResult
private func moveUntil(isNot char: UnicodeScalar) -> Int {
var move_idx = cIdx
var count = 0
while move_idx < eIdx {
guard string[move_idx] != char else { break }
move_idx = string.index(after: move_idx)
count += 1
}
cIdx = move_idx
return count
}
/// Return a date parsed from a valid ISO8601 string
///
/// - Parameter string: source string
/// - Returns: a valid `Date` object or `nil` if date cannot be parsed
public static func date(from string: String) -> ISOParsedDate? {
guard let parser = ISOParser(string) else {
return nil
}
return (parser.parsedDate, parser.parsedTimeZone)
}
public static func parse(_ string: String, region: Region?, options: Any?) -> DateInRegion? {
let formatOptions = options as? ISOParser.Options
guard let parser = ISOParser(string, options: formatOptions),
let date = parser.parsedDate else {
return nil
}
let parsedRegion = Region(calendar: region?.calendar ?? Region.ISO.calendar,
zone: (region?.timeZone ?? parser.parsedTimeZone ?? Region.ISO.timeZone),
locale: region?.locale ?? Region.ISO.locale)
return DateInRegion(date, region: parsedRegion)
}
}