简体   繁体   中英

How to slice an array into a custom time period?

I am using this Array extension to slice [HKQuantitySample] eg

let test = samplesWithoutDups.sliced(by: [.year, .month, .day], for: \.startDate )

which works well. But now I am needing to group the samples into a different period of time: 6pm the day before to 11:59pm. How can I do this?

extension Array {
  func sliced(by dateComponents: Set<Calendar.Component>, for key: KeyPath<Element, Date>) -> [Date: [Element]] {
    let initial: [Date: [Element]] = [:]
    let groupedByDateComponents = reduce(into: initial) { acc, cur in
      let components = Calendar.current.dateComponents(dateComponents, from: cur[keyPath: key])
      let date = Calendar.current.date(from: components)!
      let existing = acc[date] ?? []
      acc[date] = existing + [cur]
    }

    return groupedByDateComponents
  }
}

Ok, I thought this was an interesting puzzle, so I decided to tackle it.

My working assumption is that for sleep data, records for "tonight" would be any records with a timestamp from the earliest bedtime today until that bedtime the following day. So if the earliest bedtime is 6:00 PM, times earlier than 6:00 PM today would be counted as sleep from the previous night.

Below is a Mac Command Line tool main.swift file that includes a func slicedByDay(cutoffInterval: for:) -> [Date: [Element] function very much like the function in your question. Instead of taking a Calendar component to decide how to break up the Elements in the array, it takes a Time interval that is the number of seconds since midnight for the cutoff time (The bedtime.)

It outputs a dictionary which will contain a key/value pair for every calendar day that has sleep records that fall in the "sleep time" for that day.

The code then builds an array of test structures containing dates and strings over a 20 day period. It creates from 5 to 20 sleep readings for each date in the 20 day period, each of which could be at any random time that calendar day.

The test code uses Array.slicedByDay(cutoffInterval: for:) -> [Date: [Element] to slice the array into a dictionary. It then maps the dictionary back into an array of another struct type, SlicedDatesStruct. That is a struct that contains a Date for a slice of sleep readings, and then an array of those sleep readings. It sorts the array of SlicedDatesStruct s by sleep night, and for each SlicedDatesStruct entry, it also sorts the array of DateStruct objects by Date .

Finally, it logs the array of SlicedDatesStruct s in a way that is fairly easy to read.

//
//  main.swift
//  DatesGroupedByNight
//
//  Created by Duncan Champney on 1/23/21.
//  Copyright 2021 Duncan Champney
//  Licensed under MIT license
/*

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/


import Foundation

extension Array {
    /**
     func slicedByDay(cutoffInterval: for:) -> [Date: [Element]

     Slices an Array of objects of type `Element` that include a Date object into a dictinary of type [Date: [Element]

     - Parameter cutoffInterval: A cutoff time, in seconds since midnight. Elements with Dates up to that cutoff will be grouped in that date's calendar day. Elements with Dates after the cuttoff wil be grouped in the following calendar day.

     if you pass in a cutoffInterval for 6PM, slicedByDay() will group the Elements into those whos date KeyPath value fall between 6 PM the prior day and 6 PM on the current day

     - Parameter key: A KeyPath of the Element to use for slicing the Elements. It  must be a Date.

     - returns: A Dictonary of type [Date: [Element]. All Elements in the Elements array for a given Date key will fall between the cutoff interval for the day prior to the Date key and the cutoff interval for the current Date key
    */
    func slicedByDay(cutoffInterval: TimeInterval, for key: KeyPath<Element, Date>) -> [Date: [Element]] {
    let initial: [Date: [Element]] = [:]
    let groupedByDateComponents = reduce(into: initial) { acc, cur in


        let date = cur[keyPath: key] //Get the date from the current record
        let midnightDate = Calendar.current.startOfDay(for:date) //Calculate a midnight date for the current record.

        //Calculate the number of seconds since midnight for the current record
        let currentDatecutoffInterval = date.timeIntervalSinceReferenceDate - midnightDate.timeIntervalSinceReferenceDate

        //If the date of the current record is less than 6 PM, use today's date as the key. Otherwise, use tomorrow's date.
        let keyDate = currentDatecutoffInterval < cutoffInterval ? midnightDate : Calendar.current.date(byAdding: .day,
                                                                                                          value: 1,
                                                                                                          to: midnightDate,
                                                                                                          wrappingComponents: false)!
      let existing = acc[keyDate] ?? []
      acc[keyDate] = existing + [cur]
    }
        return groupedByDateComponents
  }
}

extension String {

    /**
     Create a random string of letters of a given length
     - parameter count: The number of letters to return

     - returns: A string of upper-case letters of `count` length
     */
    static func randomLetters(count: Int) -> String{
        var result = ""
        let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        for _ in 1...count {
            let aChar = alphabet.randomElement()!
            result.append(String(aChar))
        }
        return result
    }
}

//A sample struct used in a test of the `slicedByDay(cutoffInterval: for:) -> [Date: [Element]` function
struct DateStruct: CustomStringConvertible {
    let string: String
    let date: Date

    var description: String {
        let dateString = DateFormatter.localizedString(from: self.date, dateStyle: .short, timeStyle: .short)
        return "DateStruct(date:\(dateString), string: \(self.string))"
    }
}

//A struct use to hold an array of DateStruct objects that fall between the previous day's cutoff time and the cutoff time on the current `date`
struct SlicedDatesStruct: CustomStringConvertible {
    let date: Date
    let dateStructs: [DateStruct]
    var description: String {
        let dateString = DateFormatter.localizedString(from: self.date, dateStyle: .short, timeStyle: .short)
        var result = "Date: \(dateString)\n"
        for dateStruct in dateStructs {
            result.append("    " + dateStruct.description + "\n")
        }
        return result
    }

}


//First create an array of DateStruct objects

var dateStructArray = [DateStruct]()
let midnightDate = Calendar.current.startOfDay(for:Date()) //Calculate a Date for Midnight tonight.

let twentyfourHourInterval: TimeInterval = 24 * 60 * 60

var thisDate = midnightDate

//Create entries for 20 different dates
for _ in 1...20 {

    //Create between 5 and 20 entries for each date
    for _ in 1...Int.random(in: 5...20) {

        //Calculate a time interval for a random time any time in `thisDate`
        let randomInterval = Double.random(in: 0..<twentyfourHourInterval)

        //Create a Date for the randome time in `thisDate`
        let randomDate = thisDate.addingTimeInterval(randomInterval)

        //Create a random string for this DateStruct
        let aString = "Random String " + String.randomLetters(count: 5)
        let aDateStruct = DateStruct(string: aString, date: randomDate)
        dateStructArray.append(aDateStruct)
    }
    //Advance `thisDate` to the next calendar day
    thisDate = Calendar.current.date(byAdding: .day,
                                             value: 1,
                                             to: thisDate,
                                             wrappingComponents: false)!
}


//Now let's test `Array.slicedByDay(cutoffInterval: for:)` with a cutoff interval for 6:00 PM
let sixPMInterval: TimeInterval = 18 * 60 * 60 //Calculate the number of seconds to 6PM

//Slice up our dateStructArray
let slicedDateStructs = dateStructArray.slicedByDay(cutoffInterval: sixPMInterval, for: \.date)


//slicedDateStructs is a dictionary, which is unordered. Repackage the slices into an array of `SlicedDatesStruct`s, sorted by their Date keys
var dateSructSlices = [SlicedDatesStruct]()

let sortedDates = slicedDateStructs.keys.sorted(by:<)

let dateStructSlices: [SlicedDatesStruct] = sortedDates.map { aDate in
    return SlicedDatesStruct(date: aDate,
                             dateStructs: slicedDateStructs[aDate]!.sorted{$0.date<$1.date}
    )
}

for aSlice in dateStructSlices {
    print(aSlice)
}

Some sample output from a test run:

Date: 1/23/21, 12:00 AM
    DateStruct(date:1/23/21, 12:09 AM, string: Random String QWOJX)
    DateStruct(date:1/23/21, 12:35 AM, string: Random String TRWUR)
    DateStruct(date:1/23/21, 5:39 AM, string: Random String KEHWV)
    DateStruct(date:1/23/21, 7:28 AM, string: Random String UDNRK)
    DateStruct(date:1/23/21, 8:03 AM, string: Random String UWGTN)
    DateStruct(date:1/23/21, 8:46 AM, string: Random String LHOHY)
    DateStruct(date:1/23/21, 10:26 AM, string: Random String YQFEL)
    DateStruct(date:1/23/21, 3:47 PM, string: Random String TJUDN)
    DateStruct(date:1/23/21, 5:09 PM, string: Random String PGBDT)

Date: 1/24/21, 12:00 AM
    DateStruct(date:1/23/21, 7:46 PM, string: Random String CRULK)
    DateStruct(date:1/24/21, 2:00 AM, string: Random String NFJBC)
    DateStruct(date:1/24/21, 2:13 AM, string: Random String TROKQ)
    DateStruct(date:1/24/21, 2:49 AM, string: Random String OJJFT)
    DateStruct(date:1/24/21, 5:50 AM, string: Random String BXYOG)
    DateStruct(date:1/24/21, 7:09 AM, string: Random String LJUKP)
    DateStruct(date:1/24/21, 11:00 AM, string: Random String LAEZW)
    DateStruct(date:1/24/21, 11:01 AM, string: Random String JDNYH)
    DateStruct(date:1/24/21, 11:06 AM, string: Random String MEJBR)
    DateStruct(date:1/24/21, 11:47 AM, string: Random String WCWTP)
    DateStruct(date:1/24/21, 12:08 PM, string: Random String MVHLU)
    DateStruct(date:1/24/21, 12:24 PM, string: Random String ENHID)
    DateStruct(date:1/24/21, 12:34 PM, string: Random String EKLKP)
    DateStruct(date:1/24/21, 1:15 PM, string: Random String EBUFU)
    DateStruct(date:1/24/21, 1:45 PM, string: Random String EOJEB)
    DateStruct(date:1/24/21, 5:39 PM, string: Random String OTYBZ)

Date: 1/25/21, 12:00 AM
    DateStruct(date:1/24/21, 6:17 PM, string: Random String XMTQM)
    DateStruct(date:1/24/21, 6:25 PM, string: Random String UOFTJ)
    DateStruct(date:1/24/21, 7:15 PM, string: Random String EOIOL)
    DateStruct(date:1/24/21, 10:22 PM, string: Random String SNDQP)
    DateStruct(date:1/25/21, 4:58 AM, string: Random String AGNKJ)
    DateStruct(date:1/25/21, 7:14 AM, string: Random String CRUXZ)
    DateStruct(date:1/25/21, 7:15 AM, string: Random String FXTYX)
    DateStruct(date:1/25/21, 7:43 AM, string: Random String AXBBN)
    DateStruct(date:1/25/21, 8:09 AM, string: Random String KPSDN)
    DateStruct(date:1/25/21, 12:08 PM, string: Random String PLAYD)
    DateStruct(date:1/25/21, 12:08 PM, string: Random String URSHQ)
    DateStruct(date:1/25/21, 1:32 PM, string: Random String NCJYE)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM