diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsTimeRangeCard.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsTimeRangeCard.swift index 31f6b36f27b..7803a061ef8 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsTimeRangeCard.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsTimeRangeCard.swift @@ -31,14 +31,14 @@ struct AnalyticsTimeRangeCard: View { createTimeRangeContent() .sheet(isPresented: $showTimeRangeSelectionView) { SelectionList(title: Localization.timeRangeSelectionTitle, - items: AnalyticsHubTimeRangeSelection.SelectionType.allCases, + items: Range.allCases, contentKeyPath: \.description, selected: internalSelectionBinding()) { selection in usageTracksEventEmitter.interacted() ServiceLocator.analytics.track(event: .AnalyticsHub.dateRangeOptionSelected(selection.tracksIdentifier)) } .sheet(isPresented: $showCustomRangeSelectionView) { - RangedDatePicker() { start, end in + RangedDatePicker(startDate: selectionType.startDate, endDate: selectionType.endDate) { start, end in showTimeRangeSelectionView = false // Dismiss the initial sheet for a smooth transition self.selectionType = .custom(start: start, end: end) } @@ -96,26 +96,19 @@ struct AnalyticsTimeRangeCard: View { /// Tracks the range selection internally to determine if the custom range selection should be presented or not. /// If custom range selection is not needed, the internal selection is forwarded to `selectionType`. /// - private func internalSelectionBinding() -> Binding { + private func internalSelectionBinding() -> Binding { .init( get: { - // Temporary - switch selectionType { - // If a `custom` case is set return one with nil values so the Custom row is selected - case .custom: - return .custom(start: nil, end: nil) - default: - return selectionType - } + return selectionType.asTimeCardRange }, set: { newValue in switch newValue { - // If we get a `custom` case with nil dates it is because we need to present the custom range selection - case .custom(start: nil, end: nil): + // If we get a `custom` case it is because we need to present the custom range selection + case .custom: showCustomRangeSelectionView = true default: // Any other selection should be forwarded to our parent binding. - selectionType = newValue + selectionType = newValue.asAnalyticsHubRange } } ) @@ -154,3 +147,39 @@ struct TimeRangeCard_Previews: PreviewProvider { AnalyticsTimeRangeCard(viewModel: viewModel, selectionType: .constant(.monthToDate)) } } + + +extension AnalyticsTimeRangeCard { + enum Range: CaseIterable { + case custom + case today + case yesterday + case lastWeek + case lastMonth + case lastQuarter + case lastYear + case weekToDate + case monthToDate + case quarterToDate + case yearToDate + + /// Wee need to provide a custom `allCases` in order to evict `.custom` while the feature flag is active. + /// We should delete this once the feature flag has been removed. + /// + static var allCases: [Range] { + [ + ServiceLocator.featureFlagService.isFeatureFlagEnabled(.analyticsHub) ? .custom : nil, + .today, + .yesterday, + .lastWeek, + .lastMonth, + .lastQuarter, + .lastYear, + .weekToDate, + .monthToDate, + .quarterToDate, + yearToDate + ].compactMap { $0 } + } + } +} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/Time Range/AnalyticsHubTimeRageAdapter.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/Time Range/AnalyticsHubTimeRageAdapter.swift new file mode 100644 index 00000000000..a6d142ff364 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/Time Range/AnalyticsHubTimeRageAdapter.swift @@ -0,0 +1,267 @@ +import Foundation + +/// Type that help us converting and querying common data from the ranges supported for the Analytics Hub. +/// Currently we have two range enum types, One for the list UI and one for the supported ranges in the business layer. +/// This adapter help us sharing some common meta data between those two ranges. +/// +struct AnalyticsHubRangeAdapter { + + /// Converts an `AnalyticsHubTimeRangeSelection.SelectionType` range into a `AnalyticsTimeRangeCard.Range`. + /// + static func timeCardRange(from analyticsHubRange: AnalyticsHubTimeRangeSelection.SelectionType) -> AnalyticsTimeRangeCard.Range { + switch analyticsHubRange { + case .custom: + return .custom + case .today: + return .today + case .yesterday: + return .yesterday + case .lastWeek: + return .lastWeek + case .lastMonth: + return .lastMonth + case .lastQuarter: + return .lastQuarter + case .lastYear: + return .lastYear + case .weekToDate: + return .weekToDate + case .monthToDate: + return .monthToDate + case .quarterToDate: + return .quarterToDate + case .yearToDate: + return .yearToDate + } + } + + /// Converts an `AnalyticsTimeRangeCard.Range` into a `AnalyticsHubTimeRangeSelection.SelectionType` range. + /// + static func analyticsHubRange(from timeCardRange: AnalyticsTimeRangeCard.Range) -> AnalyticsHubTimeRangeSelection.SelectionType { + switch timeCardRange { + case .custom: + return .custom(start: Date(), end: Date()) + case .today: + return .today + case .yesterday: + return .yesterday + case .lastWeek: + return .lastWeek + case .lastMonth: + return .lastMonth + case .lastQuarter: + return .lastQuarter + case .lastYear: + return .lastYear + case .weekToDate: + return .weekToDate + case .monthToDate: + return .monthToDate + case .quarterToDate: + return .quarterToDate + case .yearToDate: + return .yearToDate + } + } + + /// Returns the desciption of the provided `AnalyticsHubTimeRangeSelection.SelectionType`range. + /// + static func description(from analyticsHubRange: AnalyticsHubTimeRangeSelection.SelectionType) -> String { + switch analyticsHubRange { + case .custom: + return Localization.custom + case .today: + return Localization.today + case .yesterday: + return Localization.yesterday + case .lastWeek: + return Localization.lastWeek + case .lastMonth: + return Localization.lastMonth + case .lastQuarter: + return Localization.lastQuarter + case .lastYear: + return Localization.lastYear + case .weekToDate: + return Localization.weekToDate + case .monthToDate: + return Localization.monthToDate + case .quarterToDate: + return Localization.quarterToDate + case .yearToDate: + return Localization.yearToDate + } + } + + /// Returns the desciption of the provided `AnalyticsTimeRangeCard.Range`. + /// + static func description(from timeCardRange: AnalyticsTimeRangeCard.Range) -> String { + switch timeCardRange { + case .custom: + return Localization.custom + case .today: + return Localization.today + case .yesterday: + return Localization.yesterday + case .lastWeek: + return Localization.lastWeek + case .lastMonth: + return Localization.lastMonth + case .lastQuarter: + return Localization.lastQuarter + case .lastYear: + return Localization.lastYear + case .weekToDate: + return Localization.weekToDate + case .monthToDate: + return Localization.monthToDate + case .quarterToDate: + return Localization.quarterToDate + case .yearToDate: + return Localization.yearToDate + } + } + + /// Returns the tracks identifier of the provided `AnalyticsHubTimeRangeSelection.SelectionType`. + /// + static func tracksIdentifier(from analyticsHubRange: AnalyticsHubTimeRangeSelection.SelectionType) -> String { + switch analyticsHubRange { + case .custom: + return TracksIdentifier.custom + case .today: + return TracksIdentifier.today + case .yesterday: + return TracksIdentifier.yesterday + case .lastWeek: + return TracksIdentifier.lastWeek + case .lastMonth: + return TracksIdentifier.lastMonth + case .lastQuarter: + return TracksIdentifier.lastQuarter + case .lastYear: + return TracksIdentifier.lastYear + case .weekToDate: + return TracksIdentifier.weekToDate + case .monthToDate: + return TracksIdentifier.monthToDate + case .quarterToDate: + return TracksIdentifier.quarterToDate + case .yearToDate: + return TracksIdentifier.yearToDate + } + } + + /// Returns the tracks identifier of the provided `AnalyticsTimeRangeCard.Range`. + /// + static func tracksIdentifier(from timeCardRange: AnalyticsTimeRangeCard.Range) -> String { + switch timeCardRange { + case .custom: + return TracksIdentifier.custom + case .today: + return TracksIdentifier.today + case .yesterday: + return TracksIdentifier.yesterday + case .lastWeek: + return TracksIdentifier.lastWeek + case .lastMonth: + return TracksIdentifier.lastMonth + case .lastQuarter: + return TracksIdentifier.lastQuarter + case .lastYear: + return TracksIdentifier.lastYear + case .weekToDate: + return TracksIdentifier.weekToDate + case .monthToDate: + return TracksIdentifier.monthToDate + case .quarterToDate: + return TracksIdentifier.quarterToDate + case .yearToDate: + return TracksIdentifier.yearToDate + } + } + + /// Extracts the dates from an analytics hub range custom type. + /// + static func customDates(from analyticsHubRange: AnalyticsHubTimeRangeSelection.SelectionType) -> (start: Date, end: Date)? { + switch analyticsHubRange { + case let .custom(startDate, endDate): + return (startDate, endDate) + default: + return nil + } + } +} + +// MARK: Constants + +private extension AnalyticsHubRangeAdapter { + enum TracksIdentifier { + static let custom = "Custom" + static let today = "Today" + static let yesterday = "Yesterday" + static let lastWeek = "Last Week" + static let lastMonth = "Last Month" + static let lastQuarter = "Last Quarter" + static let lastYear = "Last Year" + static let weekToDate = "Week to Date" + static let monthToDate = "Month to Date" + static let quarterToDate = "Quarter to Date" + static let yearToDate = "Year to Date" + } + + enum Localization { + static let custom = NSLocalizedString("Custom", comment: "Title of the Analytics Hub Custom selection range") + static let today = NSLocalizedString("Today", comment: "Title of the Analytics Hub Today's selection range") + static let yesterday = NSLocalizedString("Yesterday", comment: "Title of the Analytics Hub Yesterday selection range") + static let lastWeek = NSLocalizedString("Last Week", comment: "Title of the Analytics Hub Last Week selection range") + static let lastMonth = NSLocalizedString("Last Month", comment: "Title of the Analytics Hub Last Month selection range") + static let lastQuarter = NSLocalizedString("Last Quarter", comment: "Title of the Analytics Hub Last Quarter selection range") + static let lastYear = NSLocalizedString("Last Year", comment: "Title of the Analytics Hub Last Year selection range") + static let weekToDate = NSLocalizedString("Week to Date", comment: "Title of the Analytics Hub Week to Date selection range") + static let monthToDate = NSLocalizedString("Month to Date", comment: "Title of the Analytics Hub Month to Date selection range") + static let quarterToDate = NSLocalizedString("Quarter to Date", comment: "Title of the Analytics Hub Quarter to Date selection range") + static let yearToDate = NSLocalizedString("Year to Date", comment: "Title of the Analytics Hub Year to Date selection range") + } +} + +// MARK: Convenience Extensitons +extension AnalyticsTimeRangeCard.Range { + + var description: String { + AnalyticsHubRangeAdapter.description(from: self) + } + + var tracksIdentifier: String { + AnalyticsHubRangeAdapter.tracksIdentifier(from: self) + } + + var asAnalyticsHubRange: AnalyticsHubTimeRangeSelection.SelectionType { + AnalyticsHubRangeAdapter.analyticsHubRange(from: self) + } +} + +extension AnalyticsHubTimeRangeSelection.SelectionType { + var description: String { + AnalyticsHubRangeAdapter.description(from: self) + } + + var tracksIdentifier: String { + AnalyticsHubRangeAdapter.tracksIdentifier(from: self) + } + + var asTimeCardRange: AnalyticsTimeRangeCard.Range { + AnalyticsHubRangeAdapter.timeCardRange(from: self) + } + + /// Extracts the start date from custom range type. + /// + var startDate: Date? { + AnalyticsHubRangeAdapter.customDates(from: self)?.start + } + + /// Extracts the end date from custom range type. + /// + var endDate: Date? { + AnalyticsHubRangeAdapter.customDates(from: self)?.end + } +} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/Time Range/AnalyticsHubTimeRangeSelection.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/Time Range/AnalyticsHubTimeRangeSelection.swift index c49a8bec90f..45f63fbd325 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/Time Range/AnalyticsHubTimeRangeSelection.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/Time Range/AnalyticsHubTimeRangeSelection.swift @@ -77,27 +77,9 @@ public class AnalyticsHubTimeRangeSelection { // MARK: - Time Range Selection Type extension AnalyticsHubTimeRangeSelection { - enum SelectionType: CaseIterable, Equatable, Hashable { - /// Wee need to provide a custom `allCases` because the `.custom(Date?, Date?)`case disables its synthetization. - /// - static var allCases: [AnalyticsHubTimeRangeSelection.SelectionType] { - [ - ServiceLocator.featureFlagService.isFeatureFlagEnabled(.analyticsHub) ? .custom(start: nil, end: nil) : nil, - .today, - .yesterday, - .lastWeek, - .lastMonth, - .lastQuarter, - .lastYear, - .weekToDate, - .monthToDate, - .quarterToDate, - yearToDate - ].compactMap { $0 } - } + enum SelectionType: Equatable { - // When adding a new case, remember to add it to `allCases`. - case custom(start: Date?, end: Date?) + case custom(start: Date, end: Date) case today case yesterday case lastWeek @@ -109,33 +91,6 @@ extension AnalyticsHubTimeRangeSelection { case quarterToDate case yearToDate - var description: String { - switch self { - case .custom: - return Localization.custom - case .today: - return Localization.today - case .yesterday: - return Localization.yesterday - case .lastWeek: - return Localization.lastWeek - case .lastMonth: - return Localization.lastMonth - case .lastQuarter: - return Localization.lastQuarter - case .lastYear: - return Localization.lastYear - case .weekToDate: - return Localization.weekToDate - case .monthToDate: - return Localization.monthToDate - case .quarterToDate: - return Localization.quarterToDate - case .yearToDate: - return Localization.yearToDate - } - } - /// The granularity that should be used to request stats from the given SelectedType /// var granularity: StatsGranularityV4 { @@ -171,33 +126,6 @@ extension AnalyticsHubTimeRangeSelection { } } - var tracksIdentifier: String { - switch self { - case .custom: - return "Custom" - case .today: - return "Today" - case .yesterday: - return "Yesterday" - case .lastWeek: - return "Last Week" - case .lastMonth: - return "Last Month" - case .lastQuarter: - return "Last Quarter" - case .lastYear: - return "Last Year" - case .weekToDate: - return "Week to Date" - case .monthToDate: - return "Month to Date" - case .quarterToDate: - return "Quarter to Date" - case .yearToDate: - return "Year to Date" - } - } - init(_ statsTimeRange: StatsTimeRangeV4) { switch statsTimeRange { case .today: @@ -217,12 +145,8 @@ extension AnalyticsHubTimeRangeSelection { private extension AnalyticsHubTimeRangeSelection.SelectionType { func toRangeData(referenceDate: Date, timezone: TimeZone, calendar: Calendar) -> AnalyticsHubTimeRangeData? { switch self { - case let .custom(start?, end?): + case let .custom(start, end): return AnalyticsHubCustomRangeData(start: start, end: end, timezone: timezone, calendar: calendar) - case .custom: - // Nil custom dates are not supported but can exists when the user has selected the custom range option but hasn't choosen dates yet. - // To properly fix this, we should decouple UI selection types, from ranges selection types. - return nil case .today: return AnalyticsHubTodayRangeData(referenceDate: referenceDate, timezone: timezone, calendar: calendar) case .yesterday: @@ -255,17 +179,6 @@ extension AnalyticsHubTimeRangeSelection { } enum Localization { - static let custom = NSLocalizedString("Custom", comment: "Title of the Analytics Hub Custom selection range") - static let today = NSLocalizedString("Today", comment: "Title of the Analytics Hub Today's selection range") - static let yesterday = NSLocalizedString("Yesterday", comment: "Title of the Analytics Hub Yesterday selection range") - static let lastWeek = NSLocalizedString("Last Week", comment: "Title of the Analytics Hub Last Week selection range") - static let lastMonth = NSLocalizedString("Last Month", comment: "Title of the Analytics Hub Last Month selection range") - static let lastQuarter = NSLocalizedString("Last Quarter", comment: "Title of the Analytics Hub Last Quarter selection range") - static let lastYear = NSLocalizedString("Last Year", comment: "Title of the Analytics Hub Last Year selection range") - static let weekToDate = NSLocalizedString("Week to Date", comment: "Title of the Analytics Hub Week to Date selection range") - static let monthToDate = NSLocalizedString("Month to Date", comment: "Title of the Analytics Hub Month to Date selection range") - static let quarterToDate = NSLocalizedString("Quarter to Date", comment: "Title of the Analytics Hub Quarter to Date selection range") - static let yearToDate = NSLocalizedString("Year to Date", comment: "Title of the Analytics Hub Year to Date selection range") static let selectionTitle = NSLocalizedString("Date Range", comment: "Title of the range selection list") static let noCurrentPeriodAvailable = NSLocalizedString("No current period available", comment: "A error message when it's not possible to acquire" diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/RangedDatePicker.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/RangedDatePicker.swift index c07107606fb..ef273dd54a5 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/RangedDatePicker.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/RangedDatePicker.swift @@ -14,11 +14,19 @@ struct RangedDatePicker: View { /// Start date binding variable /// - @State private var startDate = Date() + @State private var startDate: Date /// End date binding variable /// - @State private var endDate = Date() + @State private var endDate: Date + + /// Custom `init` to provide intial start and end dates. + /// + init(startDate: Date? = nil, endDate: Date? = nil, datesSelected: ((_ start: Date, _ end: Date) -> Void)? = nil) { + self._startDate = State(initialValue: startDate ?? Date()) + self._endDate = State(initialValue: endDate ?? Date()) + self.datesSelected = datesSelected + } var body: some View { NavigationView { diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 6ba5192f35b..070a09bb2be 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -657,6 +657,7 @@ 26CCBE0B2523B3650073F94D /* RefundProductsTotalTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CCBE0A2523B3650073F94D /* RefundProductsTotalTableViewCell.swift */; }; 26CCBE0D2523C2560073F94D /* RefundProductsTotalTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 26CCBE0C2523C2560073F94D /* RefundProductsTotalTableViewCell.xib */; }; 26CFDB2727357E8000AB940B /* SimplePaymentsSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CFDB2627357E8000AB940B /* SimplePaymentsSummary.swift */; }; + 26D1E9E82949818B00A7DC62 /* AnalyticsHubTimeRageAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D1E9E72949818B00A7DC62 /* AnalyticsHubTimeRageAdapter.swift */; }; 26D9E54428C107F80098DF26 /* WooFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26D9E54328C107F80098DF26 /* WooFoundation.framework */; }; 26D9E54828C10A3B0098DF26 /* Experiments.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26D9E54728C10A3B0098DF26 /* Experiments.framework */; }; 26DB7E3528636D2200506173 /* NonEditableOrderBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26DB7E3428636D2200506173 /* NonEditableOrderBanner.swift */; }; @@ -2678,6 +2679,7 @@ 26CCBE0A2523B3650073F94D /* RefundProductsTotalTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefundProductsTotalTableViewCell.swift; sourceTree = ""; }; 26CCBE0C2523C2560073F94D /* RefundProductsTotalTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefundProductsTotalTableViewCell.xib; sourceTree = ""; }; 26CFDB2627357E8000AB940B /* SimplePaymentsSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimplePaymentsSummary.swift; sourceTree = ""; }; + 26D1E9E72949818B00A7DC62 /* AnalyticsHubTimeRageAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsHubTimeRageAdapter.swift; sourceTree = ""; }; 26D9E54328C107F80098DF26 /* WooFoundation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = WooFoundation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 26D9E54728C10A3B0098DF26 /* Experiments.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Experiments.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 26DB7E3428636D2200506173 /* NonEditableOrderBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonEditableOrderBanner.swift; sourceTree = ""; }; @@ -7535,6 +7537,7 @@ B6F379672937836700718561 /* AnalyticsHubTimeRange.swift */, B66D6CEB29396A3E0075D4AF /* AnalyticsHubTimeRangeData.swift */, B6440FB5292E72DA0012D506 /* AnalyticsHubTimeRangeSelection.swift */, + 26D1E9E72949818B00A7DC62 /* AnalyticsHubTimeRageAdapter.swift */, ); path = "Time Range"; sourceTree = ""; @@ -10045,6 +10048,7 @@ 025678C125773236009D7E6C /* Collection+ShippingLabel.swift in Sources */, 456931842653E9F2009ED69D /* ShippingLabelCarrierRow.swift in Sources */, 09BE3A8E27C91E730070B69D /* BulkUpdatePriceSettingsViewModel.swift in Sources */, + 26D1E9E82949818B00A7DC62 /* AnalyticsHubTimeRageAdapter.swift in Sources */, CE32B11A20BF8E32006FBCF4 /* UIButton+Helpers.swift in Sources */, 45BBFBC5274FDCE900213001 /* HubMenu.swift in Sources */, 02A9BCD62737F73C00159C79 /* JetpackBenefitItem.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubTimeRangeSelectionTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubTimeRangeSelectionTests.swift index 2d4e4fbaef7..b6457bc3874 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubTimeRangeSelectionTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubTimeRangeSelectionTests.swift @@ -418,8 +418,8 @@ final class AnalyticsHubTimeRangeSelectionTests: XCTestCase { func test_custom_ranges_generates_expected_descriptions() throws { // Given - let start = startDate(from: "2022-12-05") - let end = endDate(from: "2022-12-07") + let start = startDate(from: "2022-12-05") ?? Date() + let end = endDate(from: "2022-12-07") ?? Date() let timeRange = AnalyticsHubTimeRangeSelection(selectionType: .custom(start: start, end: end), timezone: testTimezone, calendar: testCalendar)