From 6f4ee467bbe149c16bdc6b453957041ad39e55f2 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Fri, 8 Nov 2024 09:07:55 +0100 Subject: [PATCH 1/8] some deadlines in timer were not considered As documented in #17, prior to Commit 5f3278c6f9a2d6fef21d63d7288739d31c81d1d3 the app's built-in Timer did not account for deadlines when submitting datapoints. While the fix covered many of the possible deadlines, it missed out on the deadlines extending 'end of day' by up to an hour. Rather than duplicating the logic to determine the daystamp on the timer, this fix uses the Daystamp entity introduced in #430, which is meant to deal with this very thing - given a submission date and a deadline, calculate which calendar date shall be reported when adding the datapoint. Fixes #228 --- BeeKit/Daystamp.swift | 2 +- BeeSwift/TimerViewController.swift | 30 ++---------------------------- 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/BeeKit/Daystamp.swift b/BeeKit/Daystamp.swift index 2e2b91ad..14d1915d 100644 --- a/BeeKit/Daystamp.swift +++ b/BeeKit/Daystamp.swift @@ -41,7 +41,7 @@ public struct Daystamp: CustomStringConvertible, Strideable, Comparable, Equatab self.init(year: year, month: month, day: day) } - init(fromDate date: Date, deadline: Int) { + public init(fromDate date: Date, deadline: Int) { let secondsAfterMidnight = Daystamp.calendar.component(.hour, from: date) * 60 * 60 + Daystamp.calendar.component(.minute, from: date) * 60 diff --git a/BeeSwift/TimerViewController.swift b/BeeSwift/TimerViewController.swift index aa824b55..f71ffc01 100644 --- a/BeeSwift/TimerViewController.swift +++ b/BeeSwift/TimerViewController.swift @@ -143,33 +143,7 @@ class TimerViewController: UIViewController { } func urtext() -> String { - // if the goal's deadline is after midnight, and it's after midnight, - // but before the deadline, - // default to entering data for the "previous" day. - let now = Date() - var offset: Double = 0 - let calendar = Calendar.current - let components = (calendar as NSCalendar).components([.hour, .minute], from: now) - let currentHour = components.hour - if self.goal.deadline > 0 && currentHour! < 6 && self.goal.deadline/3600 < currentHour! { - offset = -1 - } - - // if the goal's deadline is before midnight and has already passed for this calendar day, default to entering data for the "next" day - if self.goal.deadline < 0 { - let deadlineSecondsAfterMidnight = 24*3600 + self.goal.deadline - let deadlineHour = deadlineSecondsAfterMidnight/3600 - let deadlineMinute = (deadlineSecondsAfterMidnight % 3600)/60 - let currentMinute = components.minute - if deadlineHour < currentHour! || - (deadlineHour == currentHour! && deadlineMinute < currentMinute!) { - offset = 1 - } - } - - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "en_US") - formatter.dateFormat = "d" + let day = Daystamp(fromDate: Date(), deadline: goal.deadline).day let value: Double @@ -182,7 +156,7 @@ class TimerViewController: UIViewController { let comment = "Automatically entered from iOS timer interface" - return "\(formatter.string(from: Date(timeIntervalSinceNow: offset*24*3600))) \(value) \"\(comment)\"" + return "\(day) \(value) \"\(comment)\"" } @objc func addDatapointButtonPressed() { From 609ffdf4aa52c51b238965c9f13600ef2714d6a3 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Fri, 8 Nov 2024 09:08:22 +0100 Subject: [PATCH 2/8] public method missing documentation --- BeeKit/Daystamp.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/BeeKit/Daystamp.swift b/BeeKit/Daystamp.swift index 14d1915d..2d84c7a7 100644 --- a/BeeKit/Daystamp.swift +++ b/BeeKit/Daystamp.swift @@ -40,7 +40,12 @@ public struct Daystamp: CustomStringConvertible, Strideable, Comparable, Equatab self.init(year: year, month: month, day: day) } - + + /// Creates a Daystamp of having submitted a datapoint on a particular date given the goal's deadline + /// + /// - Parameters: + /// - date: a calendar date + /// - deadline: a ``Goal/deadline`` public init(fromDate date: Date, deadline: Int) { let secondsAfterMidnight = Daystamp.calendar.component(.hour, from: date) * 60 * 60 From cc327e731fa62c9e25d084c74702b403e02b6f0b Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Fri, 8 Nov 2024 09:08:37 +0100 Subject: [PATCH 3/8] public method missing documentation --- BeeKit/Daystamp.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/BeeKit/Daystamp.swift b/BeeKit/Daystamp.swift index 2d84c7a7..02a515bb 100644 --- a/BeeKit/Daystamp.swift +++ b/BeeKit/Daystamp.swift @@ -115,6 +115,9 @@ public struct Daystamp: CustomStringConvertible, Strideable, Comparable, Equatab // Trait: Strideable + /// how many days apart two daystamps are + /// - Parameter other: another daystamp + /// - Returns: number of days as distance between the daystamps public func distance(to other: Daystamp) -> Int { let selfDate = Daystamp.calendar.date(from: DateComponents(calendar: Daystamp.calendar, year: year, month: month, day: day))! let otherDate = Daystamp.calendar.date(from: DateComponents(calendar: Daystamp.calendar, year: other.year, month: other.month, day: other.day))! From 448969a197a44db05827eb2e5da1e7e43321acf5 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Fri, 8 Nov 2024 09:08:57 +0100 Subject: [PATCH 4/8] public property missing documentation --- BeeKit/Daystamp.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/BeeKit/Daystamp.swift b/BeeKit/Daystamp.swift index 02a515bb..ef0682ba 100644 --- a/BeeKit/Daystamp.swift +++ b/BeeKit/Daystamp.swift @@ -109,6 +109,7 @@ public struct Daystamp: CustomStringConvertible, Strideable, Comparable, Equatab // Trait: CustomStringConvertible + /// Daystamp formatted as a YYYYMMdd string public var description: String { return String(format: "%04d%02d%02d", year, month, day) } From 72c55ffbc3e6520f1857d4d9572d11d151c8916f Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Fri, 8 Nov 2024 09:13:40 +0100 Subject: [PATCH 5/8] handling first hour after midnight deadlines on the goal screen The add datapoint section of the goal screen contained logic for determining the stepper's initial value for a goal. It defaults to today with adjustments made for a goal's deadline. It contained the same logic as found on the Timer screen and contained the same logic error of not accounting for deadlines within the first hour after midnight. This fix replaces the Goal screen's duplicated logic accounting for deadlines with logic already present in the dedicated entity Daystamp. Possibly related: #473 --- BeeSwift/GoalViewController.swift | 33 +++++++++---------------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/BeeSwift/GoalViewController.swift b/BeeSwift/GoalViewController.swift index 2da1f19d..5e2f278c 100644 --- a/BeeSwift/GoalViewController.swift +++ b/BeeSwift/GoalViewController.swift @@ -209,30 +209,15 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl self.dateStepper.tintColor = UIColor.Beeminder.gray dataEntryView.addSubview(self.dateStepper) self.dateStepper.addTarget(self, action: #selector(GoalViewController.dateStepperValueChanged), for: .valueChanged) - self.dateStepper.value = 0 - - // if the goal's deadline is after midnight, and it's after midnight, - // but before the deadline, - // default to entering data for the "previous" day. - let now = Date() - let calendar = Calendar.current - let components = (calendar as NSCalendar).components([.hour, .minute], from: now) - let currentHour = components.hour - if self.goal.deadline > 0 && currentHour! < 6 && currentHour! < self.goal.deadline/3600 { - self.dateStepper.value = -1 - } - - // if the goal's deadline is before midnight and has already passed for this calendar day, default to entering data for the "next" day - if self.goal.deadline < 0 { - let deadlineSecondsAfterMidnight = 24*3600 + self.goal.deadline - let deadlineHour = deadlineSecondsAfterMidnight/3600 - let deadlineMinute = (deadlineSecondsAfterMidnight % 3600)/60 - let currentMinute = components.minute - if deadlineHour < currentHour! || - (deadlineHour == currentHour! && deadlineMinute < currentMinute!) { - self.dateStepper.value = 1 - } - } + self.dateStepper.value = { + let now = Date() + let daystampAccountingForTheGoalsDeadline = Daystamp(fromDate: now, + deadline: goal.deadline) + let daystampAssumingMidnightDeadline = Daystamp(fromDate: now, + deadline: 0) + + return Double(daystampAccountingForTheGoalsDeadline.distance(to: daystampAssumingMidnightDeadline)) + }() self.dateStepper.snp.makeConstraints { (make) -> Void in make.top.equalTo(self.dateTextField.snp.bottom).offset(elementSpacing) From 41200f9016697e4a60230f97a194bc041aa0dfca Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Fri, 8 Nov 2024 09:21:01 +0100 Subject: [PATCH 6/8] just as well specify all of year, month, and day when submitting urtext the client has the data and need not rely on the backend to use its magic around which year and month might be associated with a lone day --- BeeSwift/TimerViewController.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/BeeSwift/TimerViewController.swift b/BeeSwift/TimerViewController.swift index f71ffc01..7d1d3991 100644 --- a/BeeSwift/TimerViewController.swift +++ b/BeeSwift/TimerViewController.swift @@ -143,7 +143,11 @@ class TimerViewController: UIViewController { } func urtext() -> String { - let day = Daystamp(fromDate: Date(), deadline: goal.deadline).day + let urtextDaystamp: String = { + let daystamp = Daystamp(fromDate: Date(), deadline: goal.deadline) + + return String(format: "%04d %02d %02d", daystamp.year, daystamp.month, daystamp.day) + }() let value: Double @@ -156,7 +160,7 @@ class TimerViewController: UIViewController { let comment = "Automatically entered from iOS timer interface" - return "\(day) \(value) \"\(comment)\"" + return "\(urtextDaystamp) \(value) \"\(comment)\"" } @objc func addDatapointButtonPressed() { From b87e86c7a505f9dd047df7894c9e1efd79c74ea5 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Fri, 8 Nov 2024 09:26:22 +0100 Subject: [PATCH 7/8] same deadline logic error present in Today View extension Today View extensions have been deprecated in iOS with iOS 14 and the one in this app is expected to be removed before long. Nonetheless, for completeness and consistency, the fix has been applied to the extension of Today View as well: using Daystamp to calculate the day to be reported having accounted for the deadline --- BeeSwiftToday/TodayTableViewCell.swift | 35 +++++--------------------- 1 file changed, 6 insertions(+), 29 deletions(-) diff --git a/BeeSwiftToday/TodayTableViewCell.swift b/BeeSwiftToday/TodayTableViewCell.swift index 5df44da5..cd179002 100644 --- a/BeeSwiftToday/TodayTableViewCell.swift +++ b/BeeSwiftToday/TodayTableViewCell.swift @@ -125,36 +125,13 @@ class TodayTableViewCell: UITableViewCell { hud.mode = .indeterminate self.addDataButton.isUserInteractionEnabled = false - // if the goal's deadline is after midnight, and it's after midnight, - // but before the deadline, - // default to entering data for the "previous" day. - let now = Date() - var offset: Double = 0 - let calendar = Calendar.current - let components = (calendar as NSCalendar).components([.hour, .minute], from: now) - let currentHour = components.hour - let goalDeadline = goal.deadline - if goalDeadline > 0 && currentHour! < 6 && goalDeadline/3600 < currentHour! { - offset = -1 - } - - // if the goal's deadline is before midnight and has already passed for this calendar day, default to entering data for the "next" day - if goalDeadline < 0 { - let deadlineSecondsAfterMidnight = 24*3600 + goalDeadline - let deadlineHour = deadlineSecondsAfterMidnight/3600 - let deadlineMinute = (deadlineSecondsAfterMidnight % 3600)/60 - let currentMinute = components.minute - if deadlineHour < currentHour! || - (deadlineHour == currentHour! && deadlineMinute < currentMinute!) { - offset = 1 - } - } - - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "en_US") - formatter.dateFormat = "d" + let urtextDaystamp: String = { + let daystamp = Daystamp(fromDate: Date(), deadline: goal.deadline) + + return String(format: "%04d %02d %02d", daystamp.year, daystamp.month, daystamp.day) + }() - let params = ["urtext": "\(formatter.string(from: Date(timeIntervalSinceNow: offset*24*3600))) \(Int(self.valueStepper.value)) \"Added via iOS widget\"", "requestid": UUID().uuidString] + let params = ["urtext": "\(urtextDaystamp) \(Int(self.valueStepper.value)) \"Added via iOS widget\"", "requestid": UUID().uuidString] let slug = goal.slug Task { @MainActor in From 71b4f5294dc002aa939c721aa2ebf2f6546fd1c4 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Sun, 10 Nov 2024 17:37:45 +0100 Subject: [PATCH 8/8] clean code --- BeeKit/Daystamp.swift | 16 ++++++++++++++++ BeeSwift/GoalViewController.swift | 19 ++++++++++--------- BeeSwift/TimerViewController.swift | 6 +----- BeeSwiftToday/TodayTableViewCell.swift | 10 ++++------ 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/BeeKit/Daystamp.swift b/BeeKit/Daystamp.swift index ef0682ba..1eed6673 100644 --- a/BeeKit/Daystamp.swift +++ b/BeeKit/Daystamp.swift @@ -156,4 +156,20 @@ public struct Daystamp: CustomStringConvertible, Strideable, Comparable, Equatab // Trait: Hashable // This is generated automatically for structs by the compiler + + + /// generates the daystamp as a string for use in urtext, considering both the submission date and goal's deadline + /// - Parameters: + /// - submissionDate: calendar date on which the datapoint would be submitted + /// - goal: the goal for which the urtext is to be created + /// - Returns: string of the daystamp, suitable for use in urtext + public static func makeUrtextDaystamp(submissionDate: Date = Date(), goal: Goal) -> String { + let daystamp = Daystamp(fromDate: submissionDate, + deadline: goal.deadline) + + return String(format: "%04d %02d %02d", + daystamp.year, + daystamp.month, + daystamp.day) + } } diff --git a/BeeSwift/GoalViewController.swift b/BeeSwift/GoalViewController.swift index 5e2f278c..baa5b049 100644 --- a/BeeSwift/GoalViewController.swift +++ b/BeeSwift/GoalViewController.swift @@ -209,15 +209,7 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl self.dateStepper.tintColor = UIColor.Beeminder.gray dataEntryView.addSubview(self.dateStepper) self.dateStepper.addTarget(self, action: #selector(GoalViewController.dateStepperValueChanged), for: .valueChanged) - self.dateStepper.value = { - let now = Date() - let daystampAccountingForTheGoalsDeadline = Daystamp(fromDate: now, - deadline: goal.deadline) - let daystampAssumingMidnightDeadline = Daystamp(fromDate: now, - deadline: 0) - - return Double(daystampAccountingForTheGoalsDeadline.distance(to: daystampAssumingMidnightDeadline)) - }() + self.dateStepper.value = Self.makeInitialDateStepperValue(for: goal) self.dateStepper.snp.makeConstraints { (make) -> Void in make.top.equalTo(self.dateTextField.snp.bottom).offset(elementSpacing) @@ -498,6 +490,15 @@ class GoalViewController: UIViewController, UIScrollViewDelegate, DatapointTabl func viewForZooming(in scrollView: UIScrollView) -> UIView? { return self.goalImageView } + + private static func makeInitialDateStepperValue(date: Date = Date(), for goal: Goal) -> Double { + let daystampAccountingForTheGoalsDeadline = Daystamp(fromDate: date, + deadline: goal.deadline) + let daystampAssumingMidnightDeadline = Daystamp(fromDate: date, + deadline: 0) + + return Double(daystampAccountingForTheGoalsDeadline.distance(to: daystampAssumingMidnightDeadline)) + } // MARK: - SFSafariViewControllerDelegate diff --git a/BeeSwift/TimerViewController.swift b/BeeSwift/TimerViewController.swift index 7d1d3991..85ca53f6 100644 --- a/BeeSwift/TimerViewController.swift +++ b/BeeSwift/TimerViewController.swift @@ -143,11 +143,7 @@ class TimerViewController: UIViewController { } func urtext() -> String { - let urtextDaystamp: String = { - let daystamp = Daystamp(fromDate: Date(), deadline: goal.deadline) - - return String(format: "%04d %02d %02d", daystamp.year, daystamp.month, daystamp.day) - }() + let urtextDaystamp = Daystamp.makeUrtextDaystamp(submissionDate: Date(), goal: goal) let value: Double diff --git a/BeeSwiftToday/TodayTableViewCell.swift b/BeeSwiftToday/TodayTableViewCell.swift index cd179002..d335e6fc 100644 --- a/BeeSwiftToday/TodayTableViewCell.swift +++ b/BeeSwiftToday/TodayTableViewCell.swift @@ -125,13 +125,11 @@ class TodayTableViewCell: UITableViewCell { hud.mode = .indeterminate self.addDataButton.isUserInteractionEnabled = false - let urtextDaystamp: String = { - let daystamp = Daystamp(fromDate: Date(), deadline: goal.deadline) - - return String(format: "%04d %02d %02d", daystamp.year, daystamp.month, daystamp.day) - }() + let urtextDaystamp = Daystamp.makeUrtextDaystamp(submissionDate: Date(), goal: goal) + let value = Int(self.valueStepper.value) + let comment = "Added via iOS widget" - let params = ["urtext": "\(urtextDaystamp) \(Int(self.valueStepper.value)) \"Added via iOS widget\"", "requestid": UUID().uuidString] + let params = ["urtext": "\(urtextDaystamp) \(value) \"\(comment)\"", "requestid": UUID().uuidString] let slug = goal.slug Task { @MainActor in