diff --git a/Projects/Domain/PomodoroService/Interface/Model/PomodoroCategory.swift b/Projects/Domain/PomodoroService/Interface/Model/PomodoroCategory.swift index d02d9fd..435e0f6 100644 --- a/Projects/Domain/PomodoroService/Interface/Model/PomodoroCategory.swift +++ b/Projects/Domain/PomodoroService/Interface/Model/PomodoroCategory.swift @@ -64,24 +64,24 @@ public struct PomodoroCategory: Persistable, Equatable, Identifiable, Codable { } extension PomodoroCategory { - public var focusTimeMinute: Int { + public var focusTimeMinutes: Int { let dateComponents = DateComponents.durationFrom8601String(focusTime) - return dateComponents?.minute ?? 0 + return dateComponents?.totalMinutes ?? 0 } - public var restTimeMinute: Int { + public var restTimeMinutes: Int { let dateComponents = DateComponents.durationFrom8601String(restTime) - return dateComponents?.minute ?? 0 + return dateComponents?.totalMinutes ?? 0 } public var focusTimeSeconds: Int { let dateComponents = DateComponents.durationFrom8601String(focusTime) - return (dateComponents?.minute ?? 0) * 60 + return dateComponents?.totalSeconds ?? 0 } public var restTimeSeconds: Int { let dateComponents = DateComponents.durationFrom8601String(restTime) - return (dateComponents?.minute ?? 0) * 60 + return dateComponents?.totalSeconds ?? 0 } } diff --git a/Projects/Feature/HomeFeature/Sources/CategorySelect/CategorySelectView.swift b/Projects/Feature/HomeFeature/Sources/CategorySelect/CategorySelectView.swift index 8f02429..7c38f9d 100644 --- a/Projects/Feature/HomeFeature/Sources/CategorySelect/CategorySelectView.swift +++ b/Projects/Feature/HomeFeature/Sources/CategorySelect/CategorySelectView.swift @@ -39,7 +39,7 @@ public struct CategorySelectView: View { ForEach(store.categoryList) { category in Button( title: .init(category.title), - subtitle: "집중 \(category.focusTimeMinute)분 | 휴식 \(category.restTimeMinute)분", + subtitle: "집중 \(category.focusTimeMinutes)분 | 휴식 \(category.restTimeMinutes)분", leftIcon: category.image ) { store.send(.selectCategory(category)) diff --git a/Projects/Feature/HomeFeature/Sources/Home/HomeCore.swift b/Projects/Feature/HomeFeature/Sources/Home/HomeCore.swift index 0808d46..b2ec079 100644 --- a/Projects/Feature/HomeFeature/Sources/Home/HomeCore.swift +++ b/Projects/Feature/HomeFeature/Sources/Home/HomeCore.swift @@ -296,8 +296,8 @@ public struct HomeCore { let restedTime = DateComponents(minute: restTimeBySeconds / 60, second: restTimeBySeconds % 60).to8601DurationString() let request = FocusTimeHistory( categoryNo: selectedCategoryID, - focusedTime: focusedTime, - restedTime: restedTime, + focusedTime: focusedTime ?? "PT0M", + restedTime: restedTime ?? "PT0M", doneAt: Date() ) try await self.pomodoroService.saveFocusTimeHistory( diff --git a/Projects/Feature/HomeFeature/Sources/Home/HomeView.swift b/Projects/Feature/HomeFeature/Sources/Home/HomeView.swift index 8154fd8..182fc8b 100644 --- a/Projects/Feature/HomeFeature/Sources/Home/HomeView.swift +++ b/Projects/Feature/HomeFeature/Sources/Home/HomeView.swift @@ -64,7 +64,7 @@ public struct HomeView: View { Text("집중") .font(Typography.bodySB) .foregroundStyle(Global.Color.gray500) - Text("\(store.selectedCategory?.focusTimeMinute ?? 0)분") + Text("\(store.selectedCategory?.focusTimeMinutes ?? 0)분") .font(Typography.header3) .foregroundStyle(Alias.Color.Text.secondary) } @@ -81,7 +81,7 @@ public struct HomeView: View { Text("휴식") .font(Typography.bodySB) .foregroundStyle(Global.Color.gray500) - Text("\(store.selectedCategory?.restTimeMinute ?? 0)분") + Text("\(store.selectedCategory?.restTimeMinutes ?? 0)분") .font(Typography.header3) .foregroundStyle(Alias.Color.Text.secondary) } diff --git a/Projects/Feature/HomeFeature/Sources/TimeSelect/TimeSelectCore.swift b/Projects/Feature/HomeFeature/Sources/TimeSelect/TimeSelectCore.swift index 7c438e3..56a451d 100644 --- a/Projects/Feature/HomeFeature/Sources/TimeSelect/TimeSelectCore.swift +++ b/Projects/Feature/HomeFeature/Sources/TimeSelect/TimeSelectCore.swift @@ -83,9 +83,9 @@ public struct TimeSelectCore { if let category { switch state.mode { case .focus: - state.selectedTime = TimeItem(minute: category.focusTimeMinute) + state.selectedTime = TimeItem(minute: category.focusTimeMinutes) case .rest: - state.selectedTime = TimeItem(minute: category.restTimeMinute) + state.selectedTime = TimeItem(minute: category.restTimeMinutes) } } else { state.selectedTime = state.timeList.last diff --git a/Projects/Feature/PomodoroFeature/Sources/FocusPomodoro/FocusPomodoroCore.swift b/Projects/Feature/PomodoroFeature/Sources/FocusPomodoro/FocusPomodoroCore.swift index 8f6d26b..30ced6c 100644 --- a/Projects/Feature/PomodoroFeature/Sources/FocusPomodoro/FocusPomodoroCore.swift +++ b/Projects/Feature/PomodoroFeature/Sources/FocusPomodoro/FocusPomodoroCore.swift @@ -138,7 +138,7 @@ public struct FocusPomodoroCore { case .setupFocusTime: guard let selectedCategory = state.selectedCategory else { return .none } - state.focusTimeBySeconds = selectedCategory.focusTimeMinute * 60 + state.focusTimeBySeconds = selectedCategory.focusTimeSeconds return .none case .catSetInput: diff --git a/Projects/Feature/PomodoroFeature/Sources/RestPomodoro/RestPomodoroCore.swift b/Projects/Feature/PomodoroFeature/Sources/RestPomodoro/RestPomodoroCore.swift index cdc4eec..9e708dd 100644 --- a/Projects/Feature/PomodoroFeature/Sources/RestPomodoro/RestPomodoroCore.swift +++ b/Projects/Feature/PomodoroFeature/Sources/RestPomodoro/RestPomodoroCore.swift @@ -52,11 +52,11 @@ public struct RestPomodoroCore { ) } var minus5MinuteButtonDisabled: Bool { - guard let restTimeMinute = selectedCategory?.restTimeMinute else { return false } + guard let restTimeMinute = selectedCategory?.restTimeMinutes else { return false } return restTimeMinute <= 5 } var plus5MinuteButtonDisabled: Bool { - guard let restTimeMinute = selectedCategory?.restTimeMinute else { return false } + guard let restTimeMinute = selectedCategory?.restTimeMinutes else { return false } return restTimeMinute >= 30 } } @@ -163,7 +163,7 @@ public struct RestPomodoroCore { case .setupRestTime: guard let selectedCategory = state.selectedCategory else { return .none } - state.restTimeBySeconds = selectedCategory.restTimeMinute * 60 + state.restTimeBySeconds = selectedCategory.restTimeSeconds return .none case .catTapped: @@ -235,7 +235,7 @@ public struct RestPomodoroCore { state.changeRestTimeByMinute != 0 else { return } - let changedTimeMinute = selectedCategory.restTimeMinute + state.changeRestTimeByMinute + let changedTimeMinute = selectedCategory.restTimeMinutes + state.changeRestTimeByMinute let iso8601Duration = DateComponents(minute: changedTimeMinute).to8601DurationString() let request = EditCategoryRequest(focusTime: nil, restTime: iso8601Duration) diff --git a/Projects/Feature/PomodoroFeature/Sources/RestWaiting/RestWaitingCore.swift b/Projects/Feature/PomodoroFeature/Sources/RestWaiting/RestWaitingCore.swift index fddb77b..f5a8e54 100644 --- a/Projects/Feature/PomodoroFeature/Sources/RestWaiting/RestWaitingCore.swift +++ b/Projects/Feature/PomodoroFeature/Sources/RestWaiting/RestWaitingCore.swift @@ -42,12 +42,12 @@ public struct RestWaitingCore { } var minus5MinuteButtonDisabled: Bool { - guard let focusTimeMinute = selectedCategory?.focusTimeMinute else { return false } - return focusTimeMinute <= 10 + guard let focusTimeMinutes = selectedCategory?.focusTimeMinutes else { return false } + return focusTimeMinutes <= 10 } var plus5MinuteButtonDisabled: Bool { - guard let focusTimeMinute = selectedCategory?.focusTimeMinute else { return false } - return focusTimeMinute >= 60 + guard let focusTimeMinutes = selectedCategory?.focusTimeMinutes else { return false } + return focusTimeMinutes >= 60 } } @@ -170,7 +170,7 @@ public struct RestWaitingCore { state.changeFocusTimeByMinute != 0 else { return } - let changedTimeMinute = selectedCategory.focusTimeMinute + state.changeFocusTimeByMinute + let changedTimeMinute = selectedCategory.focusTimeMinutes + state.changeFocusTimeByMinute let iso8601Duration = DateComponents(minute: changedTimeMinute).to8601DurationString() let request = EditCategoryRequest(focusTime: iso8601Duration, restTime: nil) diff --git a/Projects/Shared/Utils/Sources/Extension/Foundation/DateComponents+Extension.swift b/Projects/Shared/Utils/Sources/Extension/Foundation/DateComponents+Extension.swift index cf5d67b..7be51e6 100644 --- a/Projects/Shared/Utils/Sources/Extension/Foundation/DateComponents+Extension.swift +++ b/Projects/Shared/Utils/Sources/Extension/Foundation/DateComponents+Extension.swift @@ -8,153 +8,62 @@ import Foundation -/* - * https://github.com/leonx98/Swift-ISO8601-DurationParser - * This extension converts ISO 8601 duration strings with the format: P[n]Y[n]M[n]DT[n]H[n]M[n]S or P[n]W into date components - * Examples: - * PT12H = 12 hours - * P3D = 3 days - * P3DT12H = 3 days, 12 hours - * P3Y6M4DT12H30M5S = 3 years, 6 months, 4 days, 12 hours, 30 minutes and 5 seconds - * P10W = 70 days - * For more information look here: http://en.wikipedia.org/wiki/ISO_8601#Durations - */ - -public extension DateComponents { - static func durationFrom8601String(_ durationString: String) -> DateComponents? { - try? Self.from8601String(durationString) +extension DateComponents { + public var totalSeconds: Int { + let secondsFromMinutes = (self.minute ?? 0) * 60 + let secondsFromHours = (self.hour ?? 0) * 3600 + let secondsFromDays = (self.day ?? 0) * 86400 + let secondsFromWeeks = (self.weekOfYear ?? 0) * 604800 + let secondsFromMonths = (self.month ?? 0) * 2629800 // 평균 월 길이 (30.44일) + let secondsFromYears = (self.year ?? 0) * 31557600 // 평균 년 길이 (365.25일) + return (self.second ?? 0) + secondsFromMinutes + secondsFromHours + secondsFromDays + secondsFromWeeks + secondsFromMonths + secondsFromYears } - // Note: Does not handle fractional values for months - // Format: PnYnMnDTnHnMnS or PnW - static func from8601String(_ durationString: String) throws -> DateComponents { - guard durationString.starts(with: "P") else { - throw DurationParsingError.invalidFormat(durationString) - } - - let durationString = String(durationString.dropFirst()) - var dateComponents = DateComponents() - - if let week = componentFor("W", in: durationString) { - // 7 day week specified in ISO 8601 standard - dateComponents.day = Int(week * 7.0) - return dateComponents - } - - let tRange = (durationString as NSString).range(of: "T", options: .literal) - let periodString: String - let timeString: String - if tRange.location == NSNotFound { - periodString = durationString - timeString = "" - } else { - periodString = (durationString as NSString).substring(to: tRange.location) - timeString = (durationString as NSString).substring(from: tRange.location + 1) - } - - // DnMnYn - let year = componentFor("Y", in: periodString) - let month = componentFor("M", in: periodString).addingFractionsFrom(year, multiplier: 12) - let day = componentFor("D", in: periodString) - - if let monthFraction = month?.truncatingRemainder(dividingBy: 1), - monthFraction != 0 { - // Representing fractional months isn't supported by DateComponents, so we don't allow it here - throw DurationParsingError.unsupportedFractionsForMonth(durationString) - } - - dateComponents.year = year?.nonFractionParts - dateComponents.month = month?.nonFractionParts - dateComponents.day = day?.nonFractionParts - - // SnMnHn - let hour = componentFor("H", in: timeString).addingFractionsFrom(day, multiplier: 24) - let minute = componentFor("M", in: timeString).addingFractionsFrom(hour, multiplier: 60) - let second = componentFor("S", in: timeString).addingFractionsFrom(minute, multiplier: 60) - dateComponents.hour = hour?.nonFractionParts - dateComponents.minute = minute?.nonFractionParts - dateComponents.second = second.map { Int($0.rounded()) } - - return dateComponents + public var totalMinutes: Int { + let minutesFromSeconds = (self.second ?? 0) / 60 + let minutesFromHours = (self.hour ?? 0) * 60 + let minutesFromDays = (self.day ?? 0) * 1440 + let minutesFromWeeks = (self.weekOfYear ?? 0) * 10080 + let minutesFromMonths = (self.month ?? 0) * 43830 // 평균 월 길이 (30.44일) + let minutesFromYears = (self.year ?? 0) * 525960 // 평균 년 길이 (365.25일) + return (self.minute ?? 0) + minutesFromSeconds + minutesFromHours + minutesFromDays + minutesFromWeeks + minutesFromMonths + minutesFromYears } - private static func componentFor(_ designator: String, in string: String) -> Double? { - // First split by the designator we're interested in, and then split by all separators. This should give us whatever's before our designator, but after the previous one. - let beforeDesignator = string.components(separatedBy: designator).first?.components(separatedBy: .separators).last - return beforeDesignator.flatMap { Double($0) } + public var totalHours: Int { + let hoursFromMinutes = (self.minute ?? 0) / 60 + let hoursFromDays = (self.day ?? 0) * 24 + let hoursFromWeeks = (self.weekOfYear ?? 0) * 168 + let hoursFromMonths = (self.month ?? 0) * 730 // 평균 월 길이 (30.44일) + let hoursFromYears = (self.year ?? 0) * 8766 // 평균 년 길이 (365.25일) + return (self.hour ?? 0) + hoursFromMinutes + hoursFromDays + hoursFromWeeks + hoursFromMonths + hoursFromYears } - enum DurationParsingError: Error { - case invalidFormat(String) - case unsupportedFractionsForMonth(String) - } -} - -private extension Optional where Wrapped == Double { - func addingFractionsFrom(_ other: Double?, multiplier: Double) -> Self { - guard let other = other else { return self } - let toAdd = other.truncatingRemainder(dividingBy: 1) * multiplier - guard let self = self else { return toAdd } - return self + toAdd + public var totalDays: Int { + let daysFromHours = (self.hour ?? 0) / 24 + let daysFromWeeks = (self.weekOfYear ?? 0) * 7 + let daysFromMonths = (self.month ?? 0) * 30 // 평균 월 길이 (30일) + let daysFromYears = (self.year ?? 0) * 365 // 평균 년 길이 (365일) + return (self.day ?? 0) + daysFromHours + daysFromWeeks + daysFromMonths + daysFromYears } -} - -private extension Double { - var nonFractionParts: Int { - Int(floor(self)) + + public var totalWeeks: Int { + let weeksFromDays = (self.day ?? 0) / 7 + let weeksFromMonths = (self.month ?? 0) * 4 // 평균 월 길이 (4주) + let weeksFromYears = (self.year ?? 0) * 52 // 평균 년 길이 (52주) + return (self.weekOfYear ?? 0) + weeksFromDays + weeksFromMonths + weeksFromYears } -} - -private extension CharacterSet { - static let separators = CharacterSet(charactersIn: "PWTYMDHMS") -} - -extension DateComponents.DurationParsingError: LocalizedError { - public var errorDescription: String? { - switch self { - case .invalidFormat(let durationString): - return "\(durationString) has an invalid format, The durationString must have a format of PnYnMnDTnHnMnS or PnW" - case .unsupportedFractionsForMonth(let durationString): - return "\(durationString) has an invalid format, fractions aren't supported for the month-position" - } + + public var totalMonths: Int { + let monthsFromDays = (self.day ?? 0) / 30 // 평균 일 길이 (30일) + let monthsFromWeeks = (self.weekOfYear ?? 0) / 4 // 평균 주 길이 (4주) + let monthsFromYears = (self.year ?? 0) * 12 + return (self.month ?? 0) + monthsFromDays + monthsFromWeeks + monthsFromYears } -} - -extension DateComponents { - public func to8601DurationString() -> String { - var components = "P" - - if let year = year { - components += "\(year)Y" - } - if let month = month { - components += "\(month)M" - } - if let day = day { - components += "\(day)D" - } - - var timeComponents = "" - - if let hour = hour { - timeComponents += "\(hour)H" - } - if let minute = minute { - timeComponents += "\(minute)M" - } - if let second = second { - timeComponents += "\(second)S" - } - - if !timeComponents.isEmpty { - components += "T" + timeComponents - } - - // ISO 8601 duration strings with only time components must start with 'PT' - if components == "P" && !timeComponents.isEmpty { - components = "PT" + timeComponents - } - - return components + + public var totalYears: Int { + let yearsFromDays = (self.day ?? 0) / 365 // 평균 일 길이 (365일) + let yearsFromWeeks = (self.weekOfYear ?? 0) / 52 // 평균 주 길이 (52주) + let yearsFromMonths = (self.month ?? 0) / 12 + return (self.year ?? 0) + yearsFromDays + yearsFromWeeks + yearsFromMonths } } diff --git a/Projects/Shared/Utils/Sources/Extension/Foundation/DateComponents+iso8601Duration.swift b/Projects/Shared/Utils/Sources/Extension/Foundation/DateComponents+iso8601Duration.swift new file mode 100644 index 0000000..34b46b8 --- /dev/null +++ b/Projects/Shared/Utils/Sources/Extension/Foundation/DateComponents+iso8601Duration.swift @@ -0,0 +1,186 @@ +// +// DateComponents+iso8601Duration.swift +// Utils +// +// Created by devMinseok on 8/23/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import Foundation + +/* + * https://github.com/leonx98/Swift-ISO8601-DurationParser + * This extension converts ISO 8601 duration strings with the format: P[n]Y[n]M[n]DT[n]H[n]M[n]S or P[n]W into date components + * Examples: + * PT12H = 12 hours + * P3D = 3 days + * P3DT12H = 3 days, 12 hours + * P3Y6M4DT12H30M5S = 3 years, 6 months, 4 days, 12 hours, 30 minutes and 5 seconds + * P10W = 70 days + * For more information look here: http://en.wikipedia.org/wiki/ISO_8601#Durations + */ +extension DateComponents { + /// ISO8601 Duration -> DateComponents + public static func durationFrom8601String(_ durationString: String) -> DateComponents? { + try? Self.from8601String(durationString) + } + + // Note: Does not handle fractional values for months + // Format: PnYnMnDTnHnMnS or PnW + static func from8601String(_ durationString: String) throws -> DateComponents { + guard durationString.starts(with: "P") else { + throw DurationParsingError.invalidFormat(durationString) + } + + let durationString = String(durationString.dropFirst()) + var dateComponents = DateComponents() + + if let week = componentFor("W", in: durationString) { + // 7 day week specified in ISO 8601 standard + dateComponents.day = Int(week * 7.0) + return dateComponents + } + + let tRange = (durationString as NSString).range(of: "T", options: .literal) + let periodString: String + let timeString: String + if tRange.location == NSNotFound { + periodString = durationString + timeString = "" + } else { + periodString = (durationString as NSString).substring(to: tRange.location) + timeString = (durationString as NSString).substring(from: tRange.location + 1) + } + + // DnMnYn + let year = componentFor("Y", in: periodString) + let month = componentFor("M", in: periodString).addingFractionsFrom(year, multiplier: 12) + let day = componentFor("D", in: periodString) + + if let monthFraction = month?.truncatingRemainder(dividingBy: 1), + monthFraction != 0 { + // Representing fractional months isn't supported by DateComponents, so we don't allow it here + throw DurationParsingError.unsupportedFractionsForMonth(durationString) + } + + dateComponents.year = year?.nonFractionParts + dateComponents.month = month?.nonFractionParts + dateComponents.day = day?.nonFractionParts + + // SnMnHn + let hour = componentFor("H", in: timeString).addingFractionsFrom(day, multiplier: 24) + let minute = componentFor("M", in: timeString).addingFractionsFrom(hour, multiplier: 60) + let second = componentFor("S", in: timeString).addingFractionsFrom(minute, multiplier: 60) + dateComponents.hour = hour?.nonFractionParts + dateComponents.minute = minute?.nonFractionParts + dateComponents.second = second.map { Int($0.rounded()) } + + return dateComponents + } + + private static func componentFor(_ designator: String, in string: String) -> Double? { + // First split by the designator we're interested in, and then split by all separators. This should give us whatever's before our designator, but after the previous one. + let beforeDesignator = string.components(separatedBy: designator).first?.components(separatedBy: .separators).last + return beforeDesignator.flatMap { Double($0) } + } + + enum DurationParsingError: Error { + case invalidFormat(String) + case unsupportedFractionsForMonth(String) + } +} + +extension Optional where Wrapped == Double { + func addingFractionsFrom(_ other: Double?, multiplier: Double) -> Self { + guard let other = other else { return self } + let toAdd = other.truncatingRemainder(dividingBy: 1) * multiplier + guard let self = self else { return toAdd } + return self + toAdd + } +} + +extension Double { + var nonFractionParts: Int { + Int(floor(self)) + } +} + +extension CharacterSet { + static let separators = CharacterSet(charactersIn: "PWTYMDHMS") +} + +extension DateComponents.DurationParsingError: LocalizedError { + public var errorDescription: String? { + switch self { + case .invalidFormat(let durationString): + return "\(durationString) has an invalid format, The durationString must have a format of PnYnMnDTnHnMnS or PnW" + case .unsupportedFractionsForMonth(let durationString): + return "\(durationString) has an invalid format, fractions aren't supported for the month-position" + } + } +} + +extension DateComponents { + /// DateComponents -> ISO8601 Duration + public func to8601DurationString() -> String? { + var durationString = "P" + + // 년, 월, 주, 일, 시간, 분, 초를 고려하여 변환 + let totalSeconds = (self.second ?? 0) + + (self.minute ?? 0) * 60 + + (self.hour ?? 0) * 3600 + + (self.day ?? 0) * 86400 + + (self.weekOfYear ?? 0) * 604800 + + (self.month ?? 0) * 2629800 // 평균 월 길이 (30.44일) + + (self.year ?? 0) * 31557600 // 평균 년 길이 (365.25일) + + if totalSeconds == 0 { + return nil + } + + var remainingSeconds = totalSeconds + + let years = remainingSeconds / 31557600 + remainingSeconds %= 31557600 + let months = remainingSeconds / 2629800 + remainingSeconds %= 2629800 + let weeks = remainingSeconds / 604800 + remainingSeconds %= 604800 + let days = remainingSeconds / 86400 + remainingSeconds %= 86400 + let hours = remainingSeconds / 3600 + remainingSeconds %= 3600 + let minutes = remainingSeconds / 60 + remainingSeconds %= 60 + let seconds = remainingSeconds + + if years > 0 { + durationString += "\(years)Y" + } + + if months > 0 { + durationString += "\(months)M" + } + + if weeks > 0 { + durationString += "\(weeks)W" + } else if days > 0 { + durationString += "\(days)D" + } + + if hours > 0 || minutes > 0 || seconds > 0 { + durationString += "T" + if hours > 0 { + durationString += "\(hours)H" + } + if minutes > 0 { + durationString += "\(minutes)M" + } + if seconds > 0 { + durationString += "\(seconds)S" + } + } + + return durationString + } +}