[Résolu] Split Array of NSDate

iLandesiLandes Membre
septembre 2015 modifié dans Objective-C, Swift, C, C++ #1

Bonjour,


 


Mon petit problème du jour. Je souhaite séparé un tableau qui contient des NSDate en plusieurs petit tableaux (1 par mois).


 


Je cherche je test un peu tout ce que je trouve sur le net mais pour le moment je n'ai rien de satisfaisant part des méthodes bourinnes rempli de for et de if bien loin de l'élégance de swift...


 


Voici un petit bout de code pour expliquer ce que je veux.



import Foundation

func formatedDate(aString: String) -> NSDate?{
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd hh:mm zzzz"
return dateFormatter.dateFromString(aString)
}

var arrayOfDates = [NSDate]()

arrayOfDates.append(formatedDate("2015-12-24 00:00 GMT")!)
arrayOfDates.append(formatedDate("2015-11-04 00:00 GMT")!)
arrayOfDates.append(formatedDate("2015-09-01 00:00 GMT")!)
arrayOfDates.append(formatedDate("2015-09-02 00:00 GMT")!)
arrayOfDates.append(formatedDate("2015-09-03 00:00 GMT")!)
arrayOfDates.append(formatedDate("2015-09-10 00:00 GMT")!)
arrayOfDates.append(formatedDate("2015-10-05 00:00 GMT")!)
arrayOfDates.append(formatedDate("2015-10-06 00:00 GMT")!)
arrayOfDates.append(formatedDate("2015-11-03 00:00 GMT")!)
print ("Unsorted : \(arrayOfDates)")

// Sort result
arrayOfDates.sortInPlace({$0.timeIntervalSince1970 < $1.timeIntervalSince1970})
print ("Sorted : \(arrayOfDates)")

// Slice

J'aimerais donc découper mon tableau en 4 tableaux, un pour chaque mois 09, 10, 11 et 12 avec à  l'intérieur les dates concernées.


Mots clés:

Réponses

  • AliGatorAliGator Membre, Modérateur
    septembre 2015 modifié #2
    Ma petite tentative à  chaud, testé rapido dans le terminal :
    extension Array {
    func split(condition: (Element,Element)->Bool) -> [[Element]] {
    let r = self.reduce( ([[Element]](), [Element]()) ) { (var acc, item) in
    if let last = acc.1.last where condition(last, item) {
    // We want to split: add the acc.1 array that was constructed to
    // the array of arrays, then restart with a new series with only the item
    return (acc.0 + [acc.1], [item])
    } else {
    // We don't want to split: continue the current series by adding the item to it
    return (acc.0, acc.1 + [item])
    }
    }
    return r.0 + [r.1]
    }
    }
    Avec cette extension, tu peux maintenant par exemple découper un tableau de chaà®nes en sous-tableaux selon la première lettre.
    let list = ["abc","dfe","afd","dvd","eve","axd","ddd"]
    list.sort(<).split { $0.characters.first != $1.characters.first }
    Bon le code n'est peut-être pas très simple à  comprendre, d'autant que j'utilise reduce de façon un peu détournée, avec un accumulateur qui est en fait un tuple de 2 valeurs... mais là  faut que je file donc je te laisse méditer sur le code et essayer de regarder ça et me poser les questions plus tard

    Le test utilise un tableau de chaà®nes, que je groupe selon leur première lettre : je trie d'abord mon tableau, puis avec ma nouvelle méthode split je dis "découpe à  chaque fois que le premier caractère de l'élément précédent est différent du premier caractère de l'élément courant", comme ça il commence un autre sous-tableau à  chaque fois que la première lettre change (d'où l'intérêt de commencer par trier).

    Vu que j'ai utilisé des Generics, tu dois pouvoir l'utiliser tout pareil avec des NSDate, en les triant par ordre chronologique d'abord si c'est pas déjà  le cas, puis en passant comme fonction de découpage à  split() une méthode qui va regarder si les 2 dates à  comparer ont un mois différent (via NSDateComponents par exemple).


    A mon avis il y a moyen d'améliorer mon implémentation de split(), sans doute en n'utilisant comme accumulateur que directement le tableau de tableau qu'on veut construire au fur et à  mesure des itérations dans reduce, plutôt que d'utiliser cette astuce pas forcément très lisible de prime abord d'avoir un accumulateur qui est en fait composé de ce tableau + du tableau de la dernière liste en cours de construction... mais bon, "c'est un exercice laissé au lecteur" comme on dit :D
  • iLandesiLandes Membre
    septembre 2015 modifié #3


    A mon avis il y a moyen d'améliorer mon implémentation de split(), sans doute en n'utilisant comme accumulateur que directement le tableau de tableau qu'on veut construire au fur et à  mesure des itérations dans reduce, plutôt que d'utiliser cette astuce pas forcément très lisible de prime abord d'avoir un accumulateur qui est en fait composé de ce tableau + du tableau de la dernière liste en cours de construction... mais bon, "c'est un exercice laissé au lecteur" comme on dit :D


     




     


    Merci beaucoup Ali pour tes réponses toujours aussi efficaces. 


     


    Pour le moment j'avoue avoir du mal à  comprendre ton implémentation de split(), je me suis rendu au plus urgent et j'ai adapté ton code à  NSDate et cela fonctionne  o:)


     


    Voici ce que cela donne :



    import Foundation

    // Extensions
    extension Array {
    func split(condition: (Element,Element)->Bool) -> [[Element]] {
    let r = self.reduce( ([[Element]](), [Element]()) ) { ( acc, item) in
    if let last = acc.1.last where condition(last, item) {
    // We want to split: add the acc.1 array that was constructed to
    // the array of arrays, then restart with a new series with only the item
    return (acc.0 + [acc.1], [item])
    } else {
    // We don't want to split: continue the current series by adding the item to it
    return (acc.0, acc.1 + [item])
    }
    }
    return r.0 + [r.1]
    }
    }

    extension NSDate : Comparable {}

    public func < (leftDate: NSDate, rightDate: NSDate) -> Bool {
    return leftDate.compare(rightDate) == NSComparisonResult.OrderedAscending &&
    !leftDate.isEqualToDate(rightDate)
    }

    public func == (leftDate: NSDate, rightDate: NSDate) -> Bool {
    return leftDate.isEqualToDate(rightDate)
    }


    // TEST
    func formatedDate(aString: String) -> NSDate?{
    let dateFormatter = NSDateFormatter()
    dateFormatter.dateFormat = "yyyy/MM/dd hh:mm zzzz"
    return dateFormatter.dateFromString(aString)
    }

    var arrayOfDates = [NSDate]()

    arrayOfDates.append(formatedDate("2015-12-24 00:00 GMT")!)
    arrayOfDates.append(formatedDate("2015-11-04 00:00 GMT")!)
    arrayOfDates.append(formatedDate("2015-09-01 00:00 GMT")!)
    arrayOfDates.append(formatedDate("2015-09-02 00:00 GMT")!)
    arrayOfDates.append(formatedDate("2015-09-03 00:00 GMT")!)
    arrayOfDates.append(formatedDate("2015-09-10 00:00 GMT")!)
    arrayOfDates.append(formatedDate("2015-10-05 00:00 GMT")!)
    arrayOfDates.append(formatedDate("2015-10-06 00:00 GMT")!)
    arrayOfDates.append(formatedDate("2015-11-03 00:00 GMT")!)

    for a in arrayOfDates {
    print ("Before : \(a)")
    }



    let sliceDates = arrayOfDates.sort(<).split {

    let calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)
    let compareDate = calendar!.compareDate($0, toDate: $1, toUnitGranularity: NSCalendarUnit.Month)
    return !(compareDate == NSComparisonResult.OrderedSame)


    }

    // Show Result
    for a in sliceDates {
    print ("After \(a)")
    }


    Ce qui donne exactement ce que souhaitais dans la console :



    Before : 2015-12-24 00:00:00 +0000
    Before : 2015-11-04 00:00:00 +0000
    Before : 2015-09-01 00:00:00 +0000
    Before : 2015-09-02 00:00:00 +0000
    Before : 2015-09-03 00:00:00 +0000
    Before : 2015-09-10 00:00:00 +0000
    Before : 2015-10-05 00:00:00 +0000
    Before : 2015-10-06 00:00:00 +0000
    Before : 2015-11-03 00:00:00 +0000
    After [2015-09-01 00:00:00 +0000, 2015-09-02 00:00:00 +0000, 2015-09-03 00:00:00 +0000, 2015-09-10 00:00:00 +0000]
    After [2015-10-05 00:00:00 +0000, 2015-10-06 00:00:00 +0000]
    After [2015-11-03 00:00:00 +0000, 2015-11-04 00:00:00 +0000]
    After [2015-12-24 00:00:00 +0000]



    Pas sûr d'être à  la hauteur pour améliorer ton code. :o


  • AliGatorAliGator Membre, Modérateur
    Bon, je vais tenter d'expliquer mon algo et mon code, mais c'est pas forcément facile à  expliquer à  l'écrit en fait :D


    Normalement, la fonction reduce() prend en premier paramètre un point de départ A, puis itère sur tous les éléments du tableau pour te permettre d'accumuler les valeurs du tableau dans cette accumulateur A, en enrichissant l'accumulateur A à  chaque itération.

    Par exemple :

    [1,2,3,4].reduce(0) { accum, item in accum + item }
    Part du point de départ 0, puis itère sur chaque nombre du tableau et pour chaque nombre, il l'ajoute tout simplement à  l'accumulateur (qui ici est un simple entier).
    Autre exemple :

    ["Hel","lo"," ","Wo","rld"].reduce(">") { accum, item in "\(accum),\(item)" }
    // ">,Hel,lo, ,Wo,rld"
    Celui-là  va commencer par mettre ">" dans l'accumulateur (qui pour cet exemple est une chaà®ne), puis à  chaque itération va remplacer l'accumulateur par "\(accum),\(item)", soit l'ancienne valeur de l'accumulateur, suivi d'une virgule puis de la valeur de l'item. Donc au début tu pars de ">", puis à  la première itération tu as accumulé ">,Hel", à  la suivante tu rajoutes "lo" et tu as donc accumulé ">,Hel,lo", etc.


    Ici dans mon cas j'ai utilisé reduce() de façon un peu plus avancée. Au lieu d'avoir un simple accumulateur de type par exemple Array<Array<NSDate>> (enfin "Array<Array<Element>>" puisque ma fonction est générique et pas spécifique à  NSDate) et d'essayer d'accumuler directement dedans, mon accumulateur est en fait un tuple, c'est à  dire un couple de variables.
    La première de ces variables à  l'intérieur du tuple de l'accumulateur est de type Array<Array<Element>> et c'est ce qu'on compte construire au final.
    Mais au fur et à  mesure qu'on va itérer sur tes éléments, c'est pas très pratique d'extraire à  chaque itération le dernier tableau de ce tableau, pour regarder dedans son dernier élément, pour enfin pouvoir le tester et le comparer avec l'élément en cours d'itération pour savoir si tu arrives à  un élément où tu dois séparer ton tableau ou pas. Ou plutôt, ça encore ça va, on pourrait demander tableauDeTableau.last?.last et s'il existe le comparer à  item, en demandant à  la fonction condition() passée en paramètre de split de nous dire s'il faut découper ici ou pas.

    Mais après, construire la nouvelle valeur de l'accumulateur en fonction de la décision n'est pas forcément très simple. S'il n'est pas temps de découper, il faut insérer l'élément en cours d'itération à  la fin du dernier tableau de ton "tableau de tableaux". S'il est temps de découper, il faut insérer un nouveau tableau (avec pour l'instant juste l'élément en cours d'itération dedans) et ajouter ce nouveau tableau à  la fin de ton tableau de tableaux.

    C'est comme ça que j'avais commencé au début, mais j'arrivais pas à  ce que je voulais et je voulais te répondre vite. Du coup j'ai un peu triché, avec ce 2ème élément de mon tuple (mais justement, à  mon avis y'a moyen d'améliorer mon code en n'utilisant pas ce 2ème élément du tuple et en faisant directement comme je viens de te décrire au dessus sans tricher).

    Du coup, pour tricher, en plus d'avoir ce tableau de tableaux dans mon accumulateur et de l'enrichir à  chaque itération, je me balade aussi avec un Array<Element>, qui est le tableau en cours de construction.
    - Pour tes NSDate, ça va être le tableau qui est en cours d'être rempli pendant que tu parcoures toutes les dates du même mois. Tant que j'ai une date qui est du même mois que le dernier élément de ce tableau, j'ajoute l'élément au tableau et je continue. Dans ce premier cas, je ne touches donc pas au Array<Array<NSDate>> (premier élément de mon tuple de mon accumulateur), et je remplis le Array<NSDate> en cours de construction (2ème élément du tuple de mon accumulateur).
    - Par contre si je change de mois entre l'item en cours et le précédent item que j'avais (le dernier inséré dans mon Array<NSDate>), alors c'est que j'en ai fini avec le mois précédent et que je m'apprête à  en commencer un autre, du coup ce Array<NSDate> (2ème élément du tuple) que j'étais en train de remplir pour le mois en cours est fini, donc je l'ajoute à  mon Array<Array<NSDate>> (1er élément du tuple), et je repars avec un 2ème élément de tuple tout neuf, qui démarre quasi vide avec juste la nouvelle date qu'on est en train de traiter et qui est d'un mois différent d'avant.

    Sur le moment j'ai réussi à  faire ce que je voulais plus rapidement en utilisant cet élément intermédiaire pour moins me prendre la tête avec la logique de double-profondeur du "tableau de tableau" pendant l'itération. J'ai ainsi une sorte de Array<NSDate> temporaire qui ne me sers que pendant l'itération. A la toute fin de la boucle, je me retrouve avec, comme résultat de reduce(), le contenu de l'accumulateur à  la fin de la boucle, c'est à  dire un tuple contenant (1) le Array<Array<NSDate>> avec les dates réparties par mois pour tous les mois complets que j'ai pu accumuler car j'ai détecté un changement de mois avec la date précédente et (2) le Array<NSDate> contenant les dates du dernier mois, que je n'ai pas encore basculé dans le Array<Array<NSDate>> car à  la fin de la boucle je n'ai pas détecté un changement de mois (mais bon comme on est à  la fin, faut bien quand même le benner dans le tableau de tableaux avec les autres). C'est pour ça qu'à  la fin je finis avec r.0 + [r.1] pour ajouter le 2ème élément du tuple aux autres tableaux du premier élément du tuple et retourner le résultat final.


    Si je prends quelques minutes, je suis sûr que je peux me passer de ce tuple et de ce Array<NSDate> intermédiaire pour faire encore plus joli et surtout compréhensible...
  • AliGatorAliGator Membre, Modérateur
    septembre 2015 modifié #5

    public func < (leftDate: NSDate, rightDate: NSDate) -> Bool {
    return leftDate.compare(rightDate) == NSComparisonResult.OrderedAscending &&
    !leftDate.isEqualToDate(rightDate)
    }

    Tiens, pourquoi rajouter la condition "!leftDate.isEqualToDate(rightDate)" ? si leftDate et rightDate étaient égales, leftDate.compare(rightDate) aurait retourné NSComparisonResult.OrderedSame de toute façon, non ? Donc du coup pas besoin de cette condition supplémentaire ?

     

    func formatedDate(aString: String) -> NSDate?{
    let dateFormatter = NSDateFormatter()
    dateFormatter.dateFormat = "yyyy/MM/dd hh:mm zzzz"
    return dateFormatter.dateFromString(aString)
    }

    var arrayOfDates = [NSDate]()

    arrayOfDates.append(formatedDate("2015-12-24 00:00 GMT")!)
    arrayOfDates.append(formatedDate("2015-11-04 00:00 GMT")!)
    arrayOfDates.append(formatedDate("2015-09-01 00:00 GMT")!)
    arrayOfDates.append(formatedDate("2015-09-02 00:00 GMT")!)
    arrayOfDates.append(formatedDate("2015-09-03 00:00 GMT")!)
    arrayOfDates.append(formatedDate("2015-09-10 00:00 GMT")!)
    arrayOfDates.append(formatedDate("2015-10-05 00:00 GMT")!)
    arrayOfDates.append(formatedDate("2015-10-06 00:00 GMT")!)
    arrayOfDates.append(formatedDate("2015-11-03 00:00 GMT")!)

    Tiens, voilà  une très bonne occasion d'utiliser map() (cf mon dernier article concernant le sujet sur mon blog). Plutôt que de créer un "var Array" (donc "mutable" puisque "var") pour lui ajouter des éléments, il est plus "Swift" de créer un tableau de tes chaà®nes représentant tes dates, puis d'utiliser "map" pour transformer ce tableau de String en tableau de NSDate :
    let dateStrings = [
    "2015-12-24 00:00 GMT",
    "2015-11-04 00:00 GMT",
    "2015-09-01 00:00 GMT",
    "2015-09-02 00:00 GMT",
    "2015-09-03 00:00 GMT",
    "2015-09-10 00:00 GMT",
    "2015-10-05 00:00 GMT",
    "2015-10-06 00:00 GMT",
    "2015-11-03 00:00 GMT"
    ]

    // transformer ton [String] en [NSDate]
    let arrayOfDates = dateStrings.map { (str: String) -> NSDate in
    let dateFormatter = NSDateFormatter()
    dateFormatter.dateFormat = "yyyy/MM/dd hh:mm zzzz"
    return dateFormatter.dateFromString(str)!
    }
    Avantage c'est qu'en plus d'être + "Swifty", tu n'as plus que des "let" et donc plus de variables "mutables" avec risque de les modifier après coup par erreur. Tu ne construis plus ton tableau de dates en créant une variable mutable à  laquelle tu ajoutes des éléments un par un, mais tu ne manipules que des variables figés ("let") et des tableaux déjà  complets. Et puis en plus, du coup tu n'as plus besoin de créer une fonction "formatedDate" juste pour ça, qui t'était utile parce que tu avais à  l'appeler autant de fois que tu avais de date à  ajouter. Là  tu peux directement utiliser le code de cette fonction comme transformation à  passer à  map().
  • AliGatorAliGator Membre, Modérateur
    Voilà , maintenant que je me suis posé j'ai pris le temps de faire une version propre, qui n'utilise qu'un accumulateur classique et devrait être un peu plus compréhensible (même s'il y a moyen de se perdre dans ces "Tableaux de Tableaux") :

    extension Array {
    func split(condition: (Element,Element)->Bool) -> [[Element]] {
    return self.reduce( [[Element]]() ) { ( acc, item) in
    if let last = acc.last?.last where condition(last, item) {
    // We WANT to split: add a new array (with only [item] in it) at the end of the accumulator
    return acc + [[item]]
    } else {
    // We DON'T WANT to split: add the item at the end of the last array of the accumulator
    let itemAddedToLastArray = (acc.last ?? []) + [item]
    return acc.dropLast() + [itemAddedToLastArray]
    }
    }
    }
    }
    Du coup je n'ai plus qu'un seul accumulateur, qui est directement le Array<Array<Element>>

    - S'il y avait un dernier élément dans le dernier tableau (acc.last?last) et qu'en comparant ce dernier élément à  l'item en cours, la closure "condition()" nous dit qu'elle veut splitter, alors on passe dans le "if".
    Dans ce cas, on veut donc démarrer un nouveau tableau à  la fin de notre tableau de tableaux. Ainsi, si on avait "01/01/1970","05/01/1970"],["02/02/1970" dans notre accumulateur, et que là  on est en train d'étudier le cas de 07/03/1970, on ne veut pas ajouter cette date dans le tableau qui contient déjà  02/02/1970, mais on veut l'ajouter dans un nouveau Array<Element> qu'on va rajouter à  la suite des autres. Donc notre nouvel accumulateur, c'est le même que l'ancien Array<Array<NSDate>>, mais avec un *tableau* ajouté en plus à  la fin, ce tableau ne contenant pour le moment qu'un seul élément, ce nouveau "07/03/1970", car on vient tout juste de commencer un nouveau tableau pour ce mois de mars.

    - Par contre si on ne veut pas splitter mais que l'élément item qu'on est en train d'analyser doit continuer la série des dates d'avant, donc que cette date est du même mois que la dernière date insérée en somme, alors on ne veut pas ajouter nouveau un Array<NSDate> à  la fin de notre accumulateur, mais plutôt insérer cette nouvelle date à  la fin du dernier tableau de l'accumulateur.
    Du coup, je prend "acc.last", qui est le dernier Array<NSDate> (donc le tableau du dernier mois qu'on a construit, dans mon exemple précédent c'est donc le tableau ["02/02/1970"]), je lui ajoutes mon item (acc.last + [item]), et j'ai ainsi le nouveau Array<NSDate> (qui est maintenant ["02/02/1970", "07/03/1970"]) à  utiliser comme dernier élément de mon accumulateur. Et du coup je peux lui dire que mon nouvel accumulateur, c'est maintenant l'ancien, auquel j'enlève le dernier élément/tableau (dropLast), et auquel je rajoute itemAddedToLastArray, qui est ce même dernier élément/tableau, mais modifié pour avoir cet item d'ajouté à  la fin...
    C'est limite ça la partie la plus tricky, car on est obligé d'enlever l'ancien élément, en dériver un nouveau qui a ton item en + dedans, puis le rajouter. Je suis sûr que c'est encore améliorable, en passant par un accumulateur "var" pour éviter ça et pour le modifier directement "in-place"... ça sera pour la version suivante :D

    Bon en pratique je fais pas "acc.last + [item]" car "acc.last" est optional et peut être nil (si jamais je viens juste de commencer et que mon accumulateur est totalement vide, il n'a même pas de dernier élément), donc faut que je me protège de ce cas. J'utilise "?? []" pour dire "si acc.last est nil, utilise un tableau vide à  la place", comme ça je retombe sur mes pieds sans planter quand l'accumulateur n'a encore aucun élément. J'aurais pu utiliser un "if let" plutôt que "??" mais pour le coup ici "??" est plus concis.
  • AliGatorAliGator Membre, Modérateur
    septembre 2015 modifié #7
    Et voilà  enfin une version qui utilise un accumulateur "var" qu'on peut modifier directement in-place (plutôt que d'avoir à  retourner un nouveau tableau à  chaque fois dérivé de l'ancien).

    Du coup le code devrait de nouveau être compréhensible :
    extension Array {
    func split(condition: (Element,Element)->Bool) -> [[Element]] {
    return self.reduce( [[Element]()] ) { ( var acc, item) in
    // We compare the last inserted element with the current item and ask condition() if we want to split here
    if let last = acc.last?.last where condition(last, item) {
    // We WANT to split: add a new array (with only [item] in it) at the end of the accumulator
    acc.append([item])
    } else {
    // We DON'T WANT to split: add the item at the end of the last array of the accumulator
    acc[acc.count-1].append(item)
    }
    return acc
    }
    }
    }
  • Merci beaucoup Ali


     


    Le code est plus compréhensible, c'st pas pour cela que j'aurais été capable de le pondre  o:)


  • Petit update préventif pour Swift 3


     


    J'ai implanté le code 




    extension Array {
    func split(condition: (Element,Element)->Bool) -> [[Element]] {
    return self.reduce( [[Element]]() ) { ( acc, item) in
    if let last = acc.last?.last where condition(last, item) {
    // We WANT to split: add a new array (with only [item] in it) at the end of the accumulator
    return acc + [[item]]
    } else {
    // We DON'T WANT to split: add the item at the end of the last array of the accumulator
    let itemAddedToLastArray = (acc.last ?? []) + [item]
    return acc.dropLast() + [itemAddedToLastArray]
    }
    }
    }
    }

    En effet depuis peu j'ai un warning sur la dernière version :  'var' parameters are deprecated and will be removed in Swift 3


Connectez-vous ou Inscrivez-vous pour répondre.