add all api to sort by month
This commit is contained in:
932
Sources/App/Libraries/SwiftDate/Formatters/ISOParser.swift
Normal file
932
Sources/App/Libraries/SwiftDate/Formatters/ISOParser.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user