How to Omit Missing Fields from Date String (day, month, year)? - swift

I have a DateFormatter where dateStyle is set to .long like so:
private let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}()
In my application, I make requests to an API and can receive some info about dates in the response body. In response, it'll look like this for example:
struct StartDate {
let day: Int?
let month: Int?
let year: Int?
}
As all the fields are optional, some fields may or may not be present. For example, I may have a year and month, but not a day. Or maybe I only have the year. A demonstration of why this is a problem:
let date = Calendar.current.date(from: DateComponents(year: 2020, month: 12))!
print(formatter.string(from: date))
We have the year and month, but not the day. I may expect it to look like December 2020, December, 2020 or even December ?, 2020. However, it decides to replace the missing day with a 1. I can't use that because it's misleading to the user. How could I solve this problem?

Maybe a bit straight-forward but still. You can try something like this:
struct StartDate {
let day: Int?
let month: Int?
let year: Int?
var string: String {
get {
let daySymbols = day == nil ? "??" : String(day!)
let monthSymbols = month == nil ? "??" : DateFormatter().monthSymbols[month! - 1]
let yearSymbols = year == nil ? "??" : String(year!)
return "\(daySymbols) \(monthSymbols) \(yearSymbols)"
}
}
}
print(StartDate(day: nil, month: 10, year: 2020).string)
//prints '?? October 2020'
print(StartDate(day: 21, month: nil, year: 2020).string)
//prints '21 ?? 2020'
print(StartDate(day: 21, month: 9, year: nil).string)
//prints '21 September ??'

Related

Find dates in current month corresponding to a given weekday

I am currently making a scheduling feature for my application. I have a view where a user is able to select what days of the week they will be available for. Now, I am looking for a way to generate an array of dates for the given weekdays for the upcoming month.
In other words, if the user selects that they will be available on Monday, the function needs to return all dates that satisfy the given predicament for the month ahead (4 date objects).
Here's what I have tried but it doesn't seem to work:
var masterSchedule = [WorkDay(weekDay: 1, startTime: 8, endTime: 16, busyHours: []), WorkDay(weekDay: 4, startTime: 8, endTime: 16, busyHours: [])]
func getDates() {
for workDay in masterSchedule {
var components = DateComponents()
components.weekday = workDay.weekDay
components.year = 2021
components.month = 8
let date = Calendar.current.date(from: components) ?? Date()
availableDays.append(date)
}
}
For some reason, the output returns 2 same dates of 2021-07-31 23:00:00 +0000.
Can someone help me?
Here's an example that prints all the Mondays in October
import UIKit
import SwiftUI
import PlaygroundSupport
let calendar = Calendar.current
let formatter = DateFormatter()
formatter.dateStyle = .short
let firstOfOctober = formatter.date(from: "10/1/2021")!
if let interval = calendar.dateInterval(of: .month, for: firstOfOctober) {
let mondays = DateComponents(weekday: 2)
calendar.enumerateDates(startingAfter: firstOfOctober,
matching: mondays, matchingPolicy: .previousTimePreservingSmallerComponents) {
date, exactMatch, stopLooking in
if let date = date {
if date < interval.end {
print(formatter.string(from: date))
} else {
stopLooking = true
}
}
}
}

How to change the hours and minutes in an existing Date object in Swift?

I need to compare two Date object to get the day difference between them, for example: 10/10 compares with today 10/7 will be 3, but the Date object returned to me from server is not aligned with the current time. There will be a few minutes difference which results in 10/10 being 2 days ahead of 10/7 because of the delay
I found a line of code that can give me a Date object of the current time, but I want to convert an existing Date object from somewhere else, how do I do it?
let today = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: Date())!
e.g. 2020-10-08 16:08:57.259580+0000 I want it to be 2020-10-08 00:00:00 +0000 something like this
Don’t use midnight. Just parse your date string first. For calendrical calculations you should always use noon. First create a custom date format to parse your date string properly.
extension Formatter {
static let iso8601: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = .init(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSSSSxx"
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter
}()
}
Then create a helper to convert your date to noon time and another one to calculate the days between two dates and set them to noon:
extension Date {
var noon: Date {
Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: self)!
}
func days(from date: Date) -> Int {
Calendar.current.dateComponents([.day], from: date.noon, to: noon).day!
}
var daysFromToday: Int { days(from: Date()) }
}
Playground testing:
let dateString = "2020-10-08 16:08:57.259580+0000"
let now = Date() // "Oct 8, 2020 at 5:56 AM"
print(Formatter.iso8601.string(from: now)) // "2020-10-08 08:56:46.179000+0000\n"
if let date = Formatter.iso8601.date(from: dateString) { // "Oct 8, 2020 at 1:08 PM"
let days = Date().days(from: date) // 0
}
let dateString = "2020-10-10 16:08:57.259580+0000"
if let date = Formatter.iso8601.date(from: dateString) {
let days = date.daysFromToday // 2
}

How to get dates for every Friday between two dates?

I currently use the following code to return an array of dates for every single day between two dates, including today's date and the last date itself. This works great.
However, how would I go about modifying what I'm already doing in
order to do the same exact thing otherwise, but instead return an
array of the date of every Friday between the dates? For example, if the function was called on Wed Oct 23rd 2019 to return every Friday until November 10th, the first date would be Fri the 25th, Nov 1st, and then Nov 8th.
How would I do the same thing as above but for the 1st of every month? If I called the function on Wed Oct 23rd 2019 to return the first of every month until December 16th. The array should have Nov 1st and Dec 1st in it.
func dates(for date: String) -> [String] {
// first get the endDate
guard var endDate = Formatter.date.date(from: date) else { return [] }
// for calendrical calculations you should use noon time
endDate = Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: endDate)!
// lets get todays noon time to start
var date = Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: Date())!
var dates: [String] = []
// while date less than or equal to end date
while date <= endDate {
// add the formatted date to the array
dates.append( Formatter.date.string(from: date))
// increment the date by one day
date = Calendar.current.date(byAdding: .day, value: 1, to: date)!
}
return dates
}
You just need to add a weekday parameter to your method and check if the weekday of the date inside the loop before adding it to your array:
extension Formatter {
static let date = DateFormatter()
}
func dates(for date: String, weekday: Int? = nil) -> [String] {
Formatter.date.locale = Locale(identifier: "en_US_POSIX")
Formatter.date.dateFormat = "yyyy-MM-dd"
// first get the endDate
guard var endDate = Formatter.date.date(from: date) else { return [] }
// for calendrical calculations you should use noon time
endDate = Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: endDate)!
// lets get todays noon time to start
var date = Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: Date())!
var dates: [String] = []
// while date less than or equal to end date
while date <= endDate {
if weekday == nil {
dates.append(Formatter.date.string(from: date))
date = Calendar.current.date(byAdding: .day, value: 1, to: date)!
} else if let weekday = weekday, Calendar.current.component(.weekday, from: date) == weekday {
// add the formatted date to the array
dates.append(Formatter.date.string(from: date))
date = Calendar.current.date(byAdding: .weekOfYear, value: 1, to: date)!
} else {
date = Calendar.current.date(byAdding: .day, value: 1, to: date)!
}
}
return dates
}
dates(for: "2019-12-25") // ["2019-10-23", "2019-10-24", "2019-10-25", "2019-10-26", "2019-10-27", "2019-10-28", "2019-10-29", "2019-10-30", "2019-10-31", "2019-11-01", "2019-11-02", "2019-11-03", "2019-11-04", "2019-11-05", "2019-11-06", "2019-11-07", "2019-11-08", "2019-11-09", "2019-11-10", "2019-11-11", "2019-11-12", "2019-11-13", "2019-11-14", "2019-11-15", "2019-11-16", "2019-11-17", "2019-11-18", "2019-11-19", "2019-11-20", "2019-11-21", "2019-11-22", "2019-11-23", "2019-11-24", "2019-11-25", "2019-11-26", "2019-11-27", "2019-11-28", "2019-11-29", "2019-11-30", "2019-12-01", "2019-12-02", "2019-12-03", "2019-12-04", "2019-12-05", "2019-12-06", "2019-12-07", "2019-12-08", "2019-12-09", "2019-12-10", "2019-12-11", "2019-12-12", "2019-12-13", "2019-12-14", "2019-12-15", "2019-12-16", "2019-12-17", "2019-12-18", "2019-12-19", "2019-12-20", "2019-12-21", "2019-12-22", "2019-12-23", "2019-12-24", "2019-12-25"]
dates(for: "2019-12-25", weekday: 6) // ["2019-10-25", "2019-11-01", "2019-11-08", "2019-11-15", "2019-11-22", "2019-11-29", "2019-12-06", "2019-12-13", "2019-12-20"]
func firstDayOfTheMonth(until date: String) -> [String] {
Formatter.date.locale = Locale(identifier: "en_US_POSIX")
Formatter.date.dateFormat = "yyyy-MM-dd"
guard let endDate = Formatter.date.date(from: date) else { return [] }
var date = Date()
var dates: [String] = []
// while date less than or equal to end date
while let firstDayOfTheMonth = Calendar.current.nextDate(after: date, matching: .init(day: 1), matchingPolicy: .nextTime), firstDayOfTheMonth <= endDate {
dates.append(Formatter.date.string(from: firstDayOfTheMonth))
date = firstDayOfTheMonth
}
return dates
}
firstDayOfTheMonth(until: "2019-12-25") // ["2019-11-01", "2019-12-01"]
You can get the weekday for any day of the week with:
let weekDayIndex = Calendar.current.component(.weekday, from: Date())
Friday happens to be day 5. You can get the name of any day with:
print(Calendar.current.weekdaySymbols[weekDayIndex])
So just loop over all of your dates and filter out anything where the weekday is not 5 and you have your answer:
func fridays(in dates: [Date]) {
dates.filter { Calendar.current.component(.weekday, from: $0) == 5 }
}

Swift - nextDate() search by day of year

Hoping for some help on this one. I'm trying to search (forwards and backwards) to find the next date that matches the day of the year
So, for instance I have a day value of 300, I know we are currently on day 237 of this year (as of writing this post, 25/08/18!), and I want to search backwards to find the previous occurrence of the 300th day of a year, and format a date from it.
I'm already extracting the 'day of year' from date using a small Date extension:
extension Date {
var dayOfYear: Int {
return Calendar.current.ordinality(of: .day, in: .year, for: self)!
}
}
Using the Calendar.nextDate() function I can search to match '.day' , but that's obviously day of month, not year:
let potentialSartDate = (Calendar.current as NSCalendar).nextDate(after: nowDate, matching: .day, value: dayOfYearValueToMatch, options: [.matchNextTime, .searchBackwards])!
Does anyone have any pointers, or approach, of how to perform a search like this build a date out of the result?
Thanks in advance!
Emile
You can get the current year component from today and use the Calendar date(from: DateComponents) method to get the 300th day of the year as follow:
extension Calendar {
static let iso8601 = Calendar(identifier: .iso8601)
}
extension Date {
var year: Int {
return Calendar.current.component(.year, from: self)
}
var last300ThDayOfYear: Date {
return Calendar.current.date(from: DateComponents(calendar: .iso8601, year: dayOfYear > 300 ? year : year - 1, day: 300))!
}
var dayOfYear: Int {
return Calendar.current.ordinality(of: .day, in: .year, for: self)!
}
}
let date = Date().last300ThDayOfYear // "Oct 27, 2017 at 12:00 AM"
print(date.dayOfYear) // 300

Swift: Substring based on a character

I'm getting information from an API and one of the fields is a DateTime in this format:
2014-12-12T14:44:18.973
I would like to remove the "T" and anything after it so in the end, I get:
2014-12-12
Any ideas or leads on how to do this with Swift? Unfortunately, I can't just create an NSDate out of directly as it throws an Exception.
In the end, I want to have something like 12-12-2014 in a string format.
Thanks!
Fortunately, You CAN just create an NSDate out of it directly as follow:
extension String {
func toDateFormattedWith(format:String)-> NSDate {
let formatter = NSDateFormatter()
// formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0) // you can set GMT time
formatter.timeZone = NSTimeZone.localTimeZone() // or as local time
formatter.dateFormat = format
return formatter.dateFromString(self)!
}
}
There is many ways to pick just the date from that string. I would use the following approach:
let myDate = "2014-12-12T14:44:18.973".componentsSeparatedByString("T")[0].toDateFormattedWith("yyyy-MM-dd") // "Dec 12, 2014, 12:00 AM"
You can also create an extension to extract that info from the Date object:
public extension NSDate {
var day: Int { return NSCalendar.currentCalendar().components(NSCalendarUnit.CalendarUnitDay, fromDate: self).day }
var month: Int { return NSCalendar.currentCalendar().components(NSCalendarUnit.CalendarUnitMonth, fromDate: self).month }
var year: Int { return NSCalendar.currentCalendar().components(NSCalendarUnit.CalendarUnitYear, fromDate: self).year }
var dateAt12am: NSDate {
return NSCalendar.currentCalendar().dateWithEra(1, year: year, month: month, day: day, hour: 0, minute: 0, second: 0, nanosecond: 0)!
}
}
let myDate1 = "2014-12-12T14:44:18.973".toDateFormattedWith("yyyy-MM-dd\'T\'HH:mm:ss.SSS").dateAt12am
println(myDate1) // "2014-12-12 02:00:00 +0000" (12am local time)
If you need reference you can use this:
This actually isn't too bad. What you're looking for is rangeOfString() to find out where "T" is. If you call .startIndex on that, you can find out where the first occurrence of that "T" is.
Once you have that, you can just create a substring from the beginning of the string to that point like this:
let date: String = dateTime.substringWithRange(Range<String.Index>(start: dateTime.startIndex, end: dateTime.rangeOfString("T")!.startIndex))
If you wanted to take that and turn it into an NSDate, you could use an NSDateFormatter:
let formatter: NSDateFormatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let newDate: NSDate = formatter.dateFromString(date)! // "Dec 12, 2014, 12:00 AM"
If you don't want that time piece at the end and just want to output the date in a different style than that returned by the API, you could add this:
formatter.dateStyle = .ShortStyle // Change this to modify the style of the date that is returned in the next line
let formattedDate: String = formatter.stringFromDate(newDate) // "12/12/14"