Swift Playground : How do I write a while function which doubles each day? - swift

I need to create a loop which will show pennies each day and total for the months end. I couldn't create the loop as the playground keeps on multiplying infinitely.

Great question! There are definitely a lot of ways to solve this problem, but if I understand your question - this is how I would solve it.
Step 1 - Date Extension
First, I would start by making an extension to Date.
extension Date {
/// The month component of the provided date.
var month: Int {
return Calendar.current.component(.month, from: self)
}
/// Exactly one day before the provided date.
var prevDay: Date {
return Calendar.current.date(byAdding: .day, value: -1, to: self)!
}
/// Exactly one day after the provided date.
var nextDay: Date {
return Calendar.current.date(byAdding: .day, value: 1, to: self)!
}
}
These variables will make some of the later work, easier.
Step 2 - Variables
Then we can setup the variables.
First, we get the start date - I am just instantiating a new Date object which defaults to right now.
Along with that, we will define a variable where we can keep track of where we are in the iteration.
Next, we need somewhere to keep track of the values over time.
Lastly, a value to hold the starting value.
let startDate = Date() // Today, now.
var iterDate = startDate
var vals: [Date: Int] = [:]
let startingValue: Int = 1
Step 3 - The Loop
Now, the fun part - the loop. This part will be documented in the code.
// Execute the loop until the end of the start date's month.
while iterDate.month == startDate.month {
// First, check if this is the first iteration -
if vals.count == 0 {
// If so, there is nothing to double, so we just set the starting value.
vals[iterDate] = startingValue
} else {
// If there are already values - get the previous days value, double it, and save.
if let val = vals[iterDate.prevDay] {
vals[iterDate] = val * 2
}
}
// Lastly, move to the next day.
iterDate = iterDate.nextDay
}
Step 4 - Final Value
Now that we have a dictionary of all of the values, as they grow, we can get the month-end value. First, sort the dictionary - then get the value. Getting the value this way means that you don't need to know the date.
let sortedVals = vals.sorted(by: { $0.0 < $1.0 })
if let monthEnd = sortedVals.last {
let monthEndVal = monthEnd.1
// Use the value, here.
}
There it is - hope that solves the problem!
Full Code
import Foundation
extension Date {
/// The month component of the provided date.
var month: Int {
return Calendar.current.component(.month, from: self)
}
/// Exactly one day before the provided date.
var prevDay: Date {
return Calendar.current.date(byAdding: .day, value: -1, to: self)!
}
/// Exactly one day after the provided date.
var nextDay: Date {
return Calendar.current.date(byAdding: .day, value: 1, to: self)!
}
}
let startDate = Date() // Today, now.
var iterDate = startDate
var vals: [Date: Int] = [:]
let startingValue: Int = 1
// Execute the loop until the end of the start date's month.
while iterDate.month == startDate.month {
// First, check if this is the first iteration -
if vals.count == 0 {
// If so, there is nothing to double, so we just set the starting value.
vals[iterDate] = startingValue
} else {
// If there are already values - get the previous days value, double it, and save.
if let val = vals[iterDate.prevDay] {
vals[iterDate] = val * 2
}
}
// Lastly, move to the next day.
iterDate = iterDate.nextDay
}
let sortedVals = vals.sorted(by: { $0.0 < $1.0 })
if let monthEnd = sortedVals.last {
let monthEndVal = monthEnd.1
// Use the value, here.
}

Related

Check if between two dates is no longer working under iOS16.3

Using iOS16.3, XCode14.2, Swift5.7.2,
Why is the following method no longer working ?
I call this method by setting date = Date() and maximumDate = Date() as well...
According to this solution, it should work - but it doesn't
public class THManager : ObservableObject {
#Published public var minimumDate: Date = Date()
#Published public var maximumDate: Date = Date()
public func isBetweenMinAndMaxDates(date: Date) -> Bool {
print(min(minimumDate, maximumDate))
print(max(minimumDate, maximumDate))
print(min(minimumDate, maximumDate)...max(minimumDate, maximumDate))
print(date)
print((min(minimumDate, maximumDate)...max(minimumDate, maximumDate)).contains(date))
return (min(minimumDate, maximumDate)...max(minimumDate, maximumDate)).contains(date)
}
}
2022-02-08 19:45:51 +0000
2023-02-03 19:45:51 +0000
2022-02-08 19:45:51 +0000...2023-02-03 19:45:51 +0000
2023-02-03 19:45:51 +0000
false
It supposed to return true ! Why does it return false ???
By the way it works if date = Date() and maximumDate = Date().addingTimeInterval(1)
Very strange, isn't it ?
There is no need for such complexity. Date objects conform to the Comparable and Equatable protocols, so testing for a date being between 2 other dates is one line:
extension Date {
func between(start: Date, end: Date) -> Bool {
return self > start && self < end
}
}
You'd use that like this:
let date = Date()
if date.betweeen(start: someDate, end: someOtherDate) {
// the date is between the start and the end
} else {
// The date is not between the start and end dates
}
The above will only return true if the date in question is not equal to start or end date. You could easily change it to match dates that match the beginning and end dates by using >= and <= instead of > and < in the comparisons.
And as discussed in the comments, Date objects have sub-millisecond precision. Two dates that appear identical may be different by a tiny fraction of a second, the best way to verify your comparisons is to convert your Dates to decimal seconds and log the seconds values. (See the Date property timeIntervalSinceReferenceDate.)
Edit:
Check out this sample code using the above exension:
extension Date {
func between(start: Date, end: Date) -> Bool {
return self > start && self < end
}
var asStringWithDecimal: String {
return DateFormatter.localizedString(from: self, dateStyle: .medium, timeStyle: .medium) + " ( \(self.timeIntervalSinceReferenceDate) seconds)"
}
}
let now = Date()
for _ in (1...5) {
let random = Double.random(in: -1.5...1.5)
let test = now.advanced(by: random)
let start = now.advanced(by: -1)
let end = now.advanced(by: 1)
let isBetween = test.between(start: start, end: end)
let isOrNot = isBetween ? "is" : "is not"
let output = "\(test.asStringWithDecimal) \n \(isOrNot) between \n \(start.asStringWithDecimal) and \n \(end.asStringWithDecimal)"
print(output)
}
That will generate 5 random dates ± 1.5 seconds from the current date, and then test each one to see if it is within 1 second of the current date. It logs the result as both Date strings and Doubles, so you can see what's happening when the seconds match (but the fractions of a second likely don't match.)

How to tell if a business is open after midnight

I've looked at this question, but a lot of it doesn't make sense nor work:
How to determine if a business is open given the hours of operation (Swift-iOS)
Some places on my list open at like 7:30 am and close the next day at 4 am. I have the times on my Parse-server listed as such:
openTime (Number): 7.5 (for 7:30 am)
closeTime (Number): and 4 for (4 am)
However, when I use the logic from the linked questions,
if now.hour! > Int(openTime) && now.hour! < Int(closeTime) {}
it keeps saying that business is closed. How could I adjust the numbers or the logic, in order to make it work for places that close late night / early morning the next day?
You can consider that closeTime must be superior to openTime, otherwise its the day after.
so it will become something like:
let realCloseTime = closeTime < openTime ? closeTime + 24 : closeTime
if now.hour! > Int(openTime) && now.hour! < Int(realCloseTime) {}
You will have a problem with minutes. Stores that open at 7:30 will report being open at 7:00. Stores that close at 11:30 will report being closed at 11:00.
Assuming that open and close are doubles.
var openTime: Double
var closeTime: Double
then
func isOpen(at date: Date = Date()) -> Bool {
guard let openDate = createDate(bySettingHours: openTime, of: date) else { return false }
guard let closeDate = createDate(bySettingHours: closeTime, of: date) else { return false }
guard let adjustedCloseDate = add24Hours(to: closeDate) else { return false }
let realCloseDate = openDate < closeDate ? closeDate : adjustedCloseDate
return openDate <= date && date <= realCloseDate
}
private func createDate(bySettingHours double: Double, of date: Date) -> Date? {
let hour = Int(floor(double)) % 24
let minute = Int(double * 30) % 30
return Calendar.current.date(bySettingHour: hour, minute: minute, second: 0, of: date)
}
private func add24Hours(to date: Date) -> Date? {
return Calendar.current.date(byAdding: .hour, value: 24, to: date)
}
I was assuming you have some kind of model like
class Business {
var openTime: Double
var closeTime: Double
}
My suggestion was to add the isOpen(at:) method here.
class Business {
var openTime: Double
var closeTime: Double
func isOpen(at date: Date = Date()) -> Bool {
// implmentation
}
}
It would be used something like this
var business = Business()
// Setup `business`
business.isOpen()
// or
let now = Date()
business.isOpen(at: now)

Swift: Creating array of Dates in a swifty way [duplicate]

This question already has an answer here:
Get an Array of Dates of the current week starting on Monday
(1 answer)
Closed 4 years ago.
I'm working with a calendar where I need to create an array of Date according to the user selection. I get a Date and I calculate the whole week. I'm currently doing that with a for loop, but I believe there are better and swiftier ways to do it. I want to go with map, but I quite don't see it:
private func selectWeek(for date: Date) {
var week = [Date]()
if let startOfWeek = date.startOfWeek {
for delta in 0 ... 6 {
if let date = Calendar.current.date(byAdding: .day, value: delta, to: startOfWeek) {
week.append(date)
}
}
calendar.selectDates(week)
}
}
You were on the right track, you can use map, you just simply need to call it on the range your loop would iterate through.
private func selectWeek(for date: Date) {
if let startOfWeek = date.startOfWeek {
let week = (0...6).compactMap{Calendar.current.date(byAdding: .day, value: $0, to: startOfWeek)}
calendar.selectDates(week)
}
}
What about:
if let startOfWeek = date.startOfWeek {
let week = (0...6).compactMap { Calendar.current.date(byAdding: .day, value: $0, to: startOfWeek) }
calendar.selectDates(week)
}
You can directly use map (or here compactMap to prevent the non nil test) on ranges.
What you wrote is fine. If you wanted to use a map statement you could do it like this:
let week: [Date]!
if let startOfWeek = date.startOfWeek {
week = Array(0...6).map {
Calendar.current.date(byAdding: .day, value: $0, to: startOfWeek)!
}
}
if week != nil {
calendar.selectDates(week)
}

Swift: check if date is the same date as any day in array of dates?

I have an array of date objects - posts. And I am looping through a month. For each day, I want to check if some date in the array is on the same day. So far I have this:
var date = month?.startOfMonth()
var end = month?.endOfMonth()
while date! <= end! {
if posts.reduce(false,{Calendar.current.isDate(date, inSameDayAsDate: post.timeStamp)}) == true {
....
}
date = Calendar.current.date(byAdding: .day, value: 1, to: date!)
}
I believe that this starts with false and for each day in posts it checks whether its in the same day and if it is it turns the result into true. However I think it also changes it back to false the next time it encounters a false value...
What I want is something that returns true if any of the dates in posts is the same as some day rather than the last one. How can I do this?
Your current code is mostly OK though I would replace reduce with contains.
if let start = month?.startOfMonth(), let end = month?.endOfMonth() {
var date = start
var found = false
while !found && date <= end {
if posts.contains { Calendar.current.isDate(date, inSameDayAs: $0.timeStamp) } {
found = true
}
date = Calendar.current.date(byAdding: .day, value: 1, to: date)
}
if found {
// We have a match
}
}
I am basically building a stack view - for every day - I create a rectangle which is blue if there is a post that day and clear if not. Thus I probably do need to know the day. However filtering the array for elements which are in the specified month seems interesting. Can you show how to do that? Perhaps I could specify the location of just those days and then fill the rest of the stackArray with clear values using insertItem atIndex
Basically, I might start with two functions, one to filter the dates by month and one to filter by day. The reason I would so this, in your case, is you for each day, you don't want to refilter all the available dates for the month (but that's just me)
func dates(_ dates: [Date], withinMonth month: Int) -> [Date] {
let calendar = Calendar.current
let components: Set<Calendar.Component> = [.month]
let filtered = dates.filter { (date) -> Bool in
calendar.dateComponents(components, from: date).month == month
}
return filtered
}
func dates(_ dates: [Date], forDay day: Int) -> [Date] {
let calendar = Calendar.current
let components: Set<Calendar.Component> = [.day]
let filtered = dates.filter { (date) -> Bool in
calendar.dateComponents(components, from: date).day == day
}
return filtered
}
You could, use a contains approach, matching both the month and day, but again, there is an overhead to consider. In the above example, you could simply check to see if the day is contained in the resulting filtered dates by month, which might be closer to you desired result
nb This is not as efficient as something like first or contains as this will iterate the entire array finding every matching element, but, it has the nice side effect of providing you with more information. For example, you could sort the resulting filters and simply iterate from the start of the month to the end, popping off each match day as it occurs, as an idea
Thinking out loud...
Another approach might be to filter the available date's by the month, as above, but then to map the result to a Set of days (ie Int), this would allow you to either iterate over each day of the month and use contains(day) to perform a simple check to see if the day is contained or not.
Equally, you could map the view's to each day and iterate of the Set, changing the state of each view.
This all depends on more context then is available, but needless to say, there are any number of ways you might approach this problem
[Updated] As rightly mentioned already, you might be more interested in having a Set of days that have at least one post, something like:
let dayComponents: Set<Calendar.Component> = [.day, .month, .year, .era]
let calendar = Calendar.current
let daysWithPosts = Set(posts.map { post in
calendar.dateComponents(dayComponents, from: post.date)
})
Then for each date you can check if it's in that set (context unchanged, mind the force unwraps):
while date! <= end! {
let currentDayComponents = calendar.dateComponents(dayComponents, from: date)
let postsFound = daysWithPosts.contains(currentDayComponents)
// <use postsFound as needed>
date = Calendar.current.date(byAdding: .day, value: 1, to: date!)
}
Original answer, adapted for multiple dates:
This should tell if there are posts on a given date's day:
func areTherePosts(in posts: [Post], fromSameDayAs date: Date) -> Bool {
let calendar = Calendar.current
let dayComponents: Set<Calendar.Component> = [.day, .month, .year, .era]
let specificDateComponents = calendar.dateComponents(dayComponents, from: date)
return posts.contains { post in
calendar.dateComponents(dayComponents, from: post.date) == specificDateComponents
}
}
Usage in your context (again, unchanged):
while date! <= end! {
let postsFound = areTherePosts(in: posts, fromSameDayAs: date!)
// <use postsFound as needed>
date = Calendar.current.date(byAdding: .day, value: 1, to: date!)
}

Trying to get data from a number of consecutive dates

I'm trying to build an array of data from 3000 consecutive days but the code will only extract data from one single date 3000 times.
When I print the date that I keep increasing (theincreasingnumberofdays) I can see that it is adding one day to the date every time it loops, but when I read out the Array after the loop it's all the same data 3000 times.
class day
{
var week: Int?
var date: Int?
var month: Int?
var year: Int?
var weekday: Int?
}
#IBAction func pressbuttontodefinearray(sender: AnyObject) {
print("button has been pressed")
if (theArrayContainingAllTheUserData.count > 1) {
print("array contains something allready and we don't do anything")
return
}
print("array is empty and we're filling it right now")
var ScoopDay = day()
var numberofloops = 0
while theArrayContainingAllTheUserData.count < 3000
{
var theincreasingnumberofdays: NSDate {
return NSCalendar.current.date(byAdding: .day, value: numberofloops, to: NSDate() as Date)! as NSDate
}
print(theincreasingnumberofdays)
let myCalendar = NSCalendar(calendarIdentifier: NSCalendar.Identifier.gregorian)!
// var thenextdate = calendar.date(byAdding: .day, value: numberofloops, to: date as Date)
numberofloops=numberofloops+1
ScoopDay.week=Int(myCalendar.component(.weekday, from: theincreasingnumberofdays as Date!))
ScoopDay.date=Int(myCalendar.component(.day, from: theincreasingnumberofdays as Date!))
ScoopDay.month=Int(myCalendar.component(.month, from: theincreasingnumberofdays as Date!))
ScoopDay.year=Int(myCalendar.component(.year, from: theincreasingnumberofdays as Date!))
ScoopDay.weekday=Int(myCalendar.component(.weekday, from: theincreasingnumberofdays as Date!))
theArrayContainingAllTheUserData.append(ScoopDay)
}
print("we're done with this looping business. Let's print it")
var placeinarray = 0
while placeinarray < 2998
{
print("Here is", placeinarray, theArrayContainingAllTheUserData[placeinarray].date, theArrayContainingAllTheUserData[placeinarray].month)
placeinarray=placeinarray+1
}
return
}
The problem is that there is one day object, named ScoopDay, and you are adding that one object to the array 3000 times. So the array ends up with 3000 references to that one single object, which contains the last values you assigned to it.
You can fix this by moving the line
var ScoopDay = day()
inside the loop. That way you will create 3000 different day objects, each with different contents.
A Swift style tip: capitalize the first letter of class names, and lowercase the first letter of variable names, so:
class Day
and
var scoopDay = Day()