Группировка объектов по Дата год и месяц


У меня есть массив дат объекты, которые необходимо сгруппировать по годам и месяцам. Структуры данных я выбрал для моей цели-это массив массивов объектов date.

Две даты объекты в одном массиве, если год и месяц совпадают.

Я поставляю двух расширений, которые я добавил в даты и форматирования, чтобы легко извлечь год и месяц на определенную дату, а также дата создания объекты более легко.

import Foundation

extension Formatter {

    static let year: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy"
        return formatter
    }()

    static let monthString: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "LLL"
        return formatter
    }()

    static let monthInt: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "LL"
        return formatter
    }()
}


extension Date {

    var monthString: String  { return Formatter.monthString.string(from: self) }
    var monthInt: Int { return Int(Formatter.monthInt.string (from:self))! }
    var yearInt: Int { return Int(Formatter.year.string(from: self))!}

    static func from(year: Int, month: Int, day: Int) -> Date {
        let gregorianCalendar = NSCalendar(calendarIdentifier: .gregorian)!

        var dateComponents = DateComponents()
        dateComponents.year = year
        dateComponents.month = month
        dateComponents.day = day

        let date:Date = gregorianCalendar.date(from: dateComponents)!
        return date
    }

    static func parse(_ string: String, format: String = "yyyy-MM-dd") -> Date {
        let dateFormatter = DateFormatter()
        dateFormatter.timeZone = NSTimeZone.default
        dateFormatter.dateFormat = format

        let date = dateFormatter.date(from: string)!
        return date
    }
}

Затем я создал вспомогательный класс со статическим методом, чтобы инкапсулировать функциональность:

class DateArrayConversionHelper {

    static func sortDateByMonth(dateArray:[Date]) -> [[Date]] {

        //create a copy of input array and sort it

        var inputArray:[Date] = dateArray
        inputArray.sort()

        //create target data structure

        var resultArray:[[Date]] = [[]]

        //set initial variable and add it to target data structure

        resultArray[0].append(inputArray[0])
        var k = 0

        for i in 1 ..< (inputArray.count) {

            if (inputArray[i].yearInt == inputArray[i-1].yearInt)
                && (inputArray[i].monthInt == inputArray[i-1].monthInt) {
                    resultArray[k].append(inputArray[i])
                }
            else {
                    k = k+1
                    resultArray.append([])
                    resultArray[k].append(inputArray[i])
                }
        }

    return resultArray
    }
}

Для тестирования:

let randomDates:[Date] = [Date.parse("2014-05-20"), Date.parse("2012-07-21"), Date.parse("2012-07-01"), Date.parse("2017-01-24"), Date.parse("2017-01-11"), Date.parse("2017-01-14"), Date.parse("2000-01-02"), Date.parse("2000-05-20")]

let resultData:[[Date]] = DateArrayConversionHelper.sortDateByMonth(dateArray: randomDates)

Я ищу способы, чтобы улучшить мой алгоритм. Я тоже еще новичок в SWIFT, так что если вы задаетесь вопросом, почему я не сделал/х использования, он обычно потому что я не знаю.

Моя конечная цель-использовать эти массивы в качестве источника для UITableView.



3311
3
задан 10 апреля 2018 в 09:04 Источник Поделиться
Комментарии
1 ответ

Я сначала сосредоточиться на

static func sortDateByMonth(dateArray:[Date]) -> [[Date]]

потому что содержит основную логику, то учтите некоторые другие точки, и, наконец,
представить альтернативное решение.



  • Ваш код, кажется, работает правильно (насколько я могу судить), с одним исключением: по-видимому,
    предполагается, что данный массив не является пустым, и он рухнет на

    resultArray[0].append(inputArray[0])

    в противном случае. Добавление

    if dateArray.isEmpty {
    return []
    }

    справа вверху функция позволит решить эту проблему.


  • В

    var inputArray:[Date] = dateArray
    inputArray.sort()

    аннотации типов не требуется, поскольку этот тип может быть автоматически выведено:

    var inputArray = dateArray
    inputArray.sort()

    Но это может быть сокращен до

    let inputArray = dateArray.sorted()

  • Точно так же,как и выше,

    var resultArray:[[Date]] = [[]]
    resultArray[0].append(inputArray[0])

    могут быть объединены в

    var resultArray = [[inputArray[0]]]

  • В

    for i in 1 ..< (inputArray.count) { ... }

    в скобках справа не нужны. Но я предпочитаю

    for i in inputArray.indices.dropFirst() { ... }

    потому что работает для других типов коллекций, где индекс не
    обязательно с нуля (например, ArraySlice).


  • Внутри цикла, инструкция

            resultArray[k].append(inputArray[i])

    появляется дважды, это может быть улучшена до

    for i in inputArray.indices.dropFirst() {
    if inputArray[i].yearInt != inputArray[i-1].yearInt
    || inputArray[i].monthInt != inputArray[i-1].monthInt {
    resultArray.append([])
    k += 1
    }
    resultArray[k].append(inputArray[i])
    }

  • Доступ к count массива O(1) операцию, поэтому можно сделать даже
    избавиться от переменной k:

    for i in inputArray.indices.dropFirst() {
    if inputArray[i].yearInt != inputArray[i-1].yearInt
    || inputArray[i].monthInt != inputArray[i-1].monthInt {
    resultArray.append([]) // Start new row
    }
    resultArray[resultArray.count - 1].append(inputArray[i])
    }

Теперь давайте попробуем сделать сравнение дат проще и эффективнее. Ваш код
требует 4 вызов метода парсинга в каждой итерации. Вообще, если вы
интересуют численные значения компонентов даты (например, год и
число месяц), затем через

let yearAndMonth = calendar.dateComponents([.year, .month], from: inputArray[i])

является более эффективным, он вычисляет оба числа в один вызов и дает вам
целыми числами вместо строк.

Но здесь мы можем сделать еще лучше: есть специальный Calendar метод проверки
если две даты равен с некоторыми из указанных компонентов:

calendar.isDate(date1, equalTo: date2, toGranularity: .month)

так что цикл становится

    let calendar = Calendar(identifier: .gregorian)
for i in inputArray.indices.dropFirst() {
if !calendar.isDate(inputArray[i-1], equalTo: inputArray[i], toGranularity: .month) {
resultArray.append([]) // Start new row
}
resultArray[resultArray.count - 1].append(inputArray[i])
}

Это делает extension Formatter и некоторые из extension Date
свойства устарело, по крайней мере для этого приложения.

Также обратите внимание, что я использовал Свифт Calendar типа вместо фундамента NSCalendar
тип.

Можно даже избавиться от индекса i в приведенном выше цикле перебором
в inputArray и сдвинутые версии этого массива, а затем
полная функция выглядит так:

static func sortDateByMonth(dateArray:[Date]) -> [[Date]] {

if dateArray.isEmpty {
return []
}

let inputArray = dateArray.sorted()
var resultArray = [[inputArray[0]]]

let calendar = Calendar(identifier: .gregorian)
for (prevDate, nextDate) in zip(inputArray, inputArray.dropFirst()) {
if !calendar.isDate(prevDate, equalTo: nextDate, toGranularity: .month) {
resultArray.append([]) // Start new row
}
resultArray[resultArray.count - 1].append(nextDate)
}
return resultArray
}


Теперь несколько дополнительных замечаний:


  • Настройка часового пояса для NSTimeZone.default не нужен, потому что это по умолчанию. С другой стороны, это
    хорошая идея, чтобы установить локаль на некоторые вполне определенные значения, такие как "en_US_POSIX",
    потому что в противном случае форматирования поведение может зависеть от региональных настроек пользователя
    (см. Технические вопросы QA1480 – NSDateFormatter и интернет-времени):

    extension Date {

    static func parse(_ string: String, format: String = "yyyy-MM-dd") -> Date {
    let dateFormatter = DateFormatter()
    dateFormatter.locale = Locale(identifier: "en_US_POSIX")
    dateFormatter.dateFormat = format
    return dateFormatter.date(from: string)!
    }
    }

    Конечно, это приведет к краху, если строка не совпадает с предоставленной формат даты.


  • В качестве альтернативы можно определить (failable) метод инициализатора:

    extension Date {

    init?(_ string: String, format: String = "yyyy-MM-dd") {
    let dateFormatter = DateFormatter()
    dateFormatter.locale = Locale(identifier: "en_US_POSIX")
    dateFormatter.dateFormat = format
    guard let date = dateFormatter.date(from: string) else { return nil }
    self = date
    }
    }

    который может быть использован в качестве

    let date = Date("2014-05-20")

    и вызывающая сторона решает, нужно ли насильно развернуть, или чтобы безопасно развернуть его
    с дополнительной привязки.


  • Ваш

    class DateArrayConversionHelper

    по-видимому, используются, чтобы обеспечить пространство для статического вспомогательную функцию.
    Если это единственная цель и ни одного случая такого рода, когда-либо созданных, тогда
    а struct с собственным init метод:

    struct DateArrayConversionHelper {
    private init() {}

    static func sortDateByMonth(dateArray:[Date]) -> [[Date]] { ... }
    // ... other helper methods ...
    }

    или бескорпусном enum

    enum DateArrayConversionHelper {

    static func sortDateByMonth(dateArray:[Date]) -> [[Date]] { ... }
    // ... other helper methods ...
    }

    хорошие альтернативы, сравнить например
    Свифт константы: структуру struct или enum
    на переполнение стека.



Наконец, альтернативный подход, который может быть проще для использования как
таблицы в представлении источника данных.

В Dictionary(grouping:by:)
инициализатор (вводит в Swift 4)


создает новый словарь, ключами которого являются группировки, возвращенных с учетом закрытия
и ценности которого являются массивы из элементов, которые возвращаются каждый ключ.

и может в вашем случае быть использованы в качестве

let calendar = Calendar(identifier: .gregorian)
let groupedDates = Dictionary(grouping: dateArray.sorted()) { (date) in
return calendar.dateInterval(of: .month, for: date)?.start ?? Date.distantPast
}

Это возвращает словарь вида [Date : [Date]] где каждый ключ является дата
представляя в начале месяца, и соответствующие значения даты в
в том же месяце.

Тогда

let availableMonths = groupedDates.keys.sorted()

есть массив дат, каждая дата обозначает начало месяца, это может использоваться, чтобы вычислить число
разделы и заголовки разделов.

За каждый месяц,

let datesInMonth = groupedDates[month]

есть массив дат в этом месяце, это может использоваться, чтобы вычислить количество строк в разделе и на
tableView(_:cellForRowAt:) метод.

3
ответ дан 11 апреля 2018 в 09:04 Источник Поделиться