TableView Sections avec des dates?

@iMrMaximus

Non,

realm.objects(Race.self).value(forKey: "date") as! [Date]

est différent de:

Array(Set(realm.objects(Race.self).value(forKey: "date") as! [Date]))

grâce au Set, il ne prends pas les valeurs identiques.
Ainsi, si tu as une liste comme ça en DB:

{"alexandre", "alexandre", "maxime", "maxime", "alice", "bob"}

le premier exemple va retourner:

{"alexandre", "alexandre", "maxime", "maxime", "alice", "bob"}

alors qu’avec le Set, ca retournera:

{"alexandre", "maxime", "alice", "bob"}

Le soucis, c’est qu’avec des dates, si tu as ça:

{"06/02/2018 14:41:34", "06/02/2018 14:57:34", "07/02/2018 10:41:34"}

moi j’aimerai trouver:

{"06/02/2018", "07/02/2018"}

mais les dates sont toutes différentes (avec les seconds, les minutes et les heures), donc le Set ne fonctionne pas.

Donc, pour ton exemple avec les dates suivantes:

let dates = ["09/02/2018", "08/02/2018", "06/02/2018", "06/02/2018", "07/02/2018", "06/02/2018", "08/02/2018"]

le Set fonctionnerait, mais avec des dates plus complète, cela ne fonctionnerait pas.

Je vais essayer ta fonction, voir si ça fonctionne avec mes données et je te tiens au courant.
Si j’ai bien compris le reduce, c’est une closure, et tu lui définis les paramètres “(res, dateS)” que tu souhaites?
Du coup, un paramètre pour une liste et l’autre pour le tableau de dates à traiter?

regarde si tu peux filtrer que les 10 premiers caractères de tes valeurs realm comme ça tu filtrera une date de type dd/mm/yyyy.
tu vois ce que je veux dire ?

En fait, l’intérêt de l’utilisation de reduce (dans mon cas) est pour éviter les doublons, comme ce que tu fais en utilisant le Set.
Donc, pas forcément besoin de reduce alors :slight_smile:.

Cependant, voici une façon de faire (sans le Set mais j’explique pourquoi après) :

let onlyDates = (realm.objects(Race.self).value(forKey: "date") as! [Date]).sorted().map { (date) -> String in
  date.toString("dd/MM/yyyy")
}
let datesForSections = onlyDates.reduce(into: [String]()) { (res, dateS) in
    if res.index(of: dateS) == nil {
        res.append(dateS)
    }
}

En essayant de supprimer les doublons en passant par un Set, il m’a mélangé les dates, donc j’ai préféré mettre le reduce quand même pour être sûr de garder l’ordre.

Et concernant le reduce, en fait, il te permet de faire n’importe quel traitement avec les éléments de la liste en faisant comme si tu étais dans une boucle for (le corps de la boucle est donc la closure ici).
J’ai donc utiliser le reduce pour redéfinir la liste en disant que dans la liste de départ (ici, le paramètre “into”), je veux le traitement suivant : pour chaque élément, sachant que la variable “res” est le résultat précédent, je fais le traitement qui suit avec la date courante (ici dateS) qui est “si dateS n’est pas dans la liste, je l’ajoute”.

@alexandre.cane Un peu comme @iMrMaximus vient de faire en fait :o

@iMrMaximus Je vais tester tout ça, et effectivement, si le Set mélange les dates, c’est préférable de garder l’ordre dès le départ, sinon je dois les re-trier après…

Mmh, je vois mieux le principe du reduce, mais du coup, si c’est le même principe qu’une boucle for, pourquoi ne pas faire une boucle for ? (Plus lisible, en tous cas pour moi)
Question de performance ? De préférence ?

Merci des explications :slight_smile:

Je ne sais pas si la fonction reduce est mieux pour les performances qu’une boucle for.
Je pense que c’est surtout mieux pour la lecture, et son écriture est beaucoup plus rapide :slight_smile:.
C’est un peu comme la fonction map. :wink:

Après oui, la boucle “for” faisant parti du B.A.-BA de la programmation, je peux comprendre ta préférence.
Mais, on y prend goût :blush:.

PS : Au fait, je disais que le Set me mélangeais les dates… je ne suis pas sûr qu’il le fasse volontairement, mais plutôt qu’il s’en fout de l’ordre final des éléments.

Ok ok, de toute façon, il faut que je me fasse à l’utilisation des clôsures, donc je vais l’utiliser :smile:

Après, je vais faire des tests de performances, pour voir ce que ça donne, je peux te tenir au courant si tu veux :slight_smile:

Le Set ne tient pas compte de l’ordre, même si tu fais une requête en triant dans un certain ordre? Dommage…

Merci pour toutes tes réponses, je test tout ça dès que je sais :smiley:

C’est normal, un Set n’est pas un tableau, mais un ensemble de valeurs organisées avec un algorithme de Hcode pour faciliter la recherche. Qu’il y ai 10 valeurs ou 1.000 valeurs dans la collection, iOS n’a besoin de quelques opérations pour vérifier si une valeur appartient à l’ensemble.

Tu peux utiliser l’opérateur Array pour créer un tableau normal à partir d’un Set, et faire ensuite tous les tris possibles et imaginables.

let tableau = Array(unSet)

Exemple de mise en oeuvre :

     let donneesBrutes = [1, 87, 54, 54, 12, 76, 1, 87]
    // Création du Set
    let setDonnees = Set(donnesBrutes)
    // Création d'un tableau ordinaire à partir du Set
    let tableauDonnees = Array(setDonnees)
    // Tri du tableau
    let tableauAvecTri = tableauDonnees.sorted{ $0<$1 }
    
    print ("Données brutes  : ", donnesBrutes)
    print ("Set des données : ", setDonnees )
    print ("tableau Set     : ", tableauDonnees)
    print ("Tableau trié    : ", tableauAvecTri)

Données brutes : [1, 87, 54, 54, 12, 76, 1, 87]
Set des données : [12, 76, 87, 1, 54]
tableau Set : [12, 76, 87, 1, 54]
Tableau trié : [1, 12, 54, 76, 87]


EDIT : En fait, je me complique la vie. On peut appliquer directement l’opérateur .sorted() à un Set pour obtenir un tableau trié, sans passer par un tableau intermédiaire.

    let tri = setDonnees.sorted{ $0<$1 }
    print ("Tri : ", tri)

Affichage :

Tri : [1, 12, 54, 76, 87]

Merci @Draken pour ces explications :slight_smile:

Si j’ai bien compris, puisque le Set fonctionne avec du HashCode, il sera plus efficace que d’utiliser un reduce qui fonctionne comme une boucle for ?

J’avais ceci qui fonctionnait:

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/yyyy"
        
let allRacesDatesStringFormated = (realm.objects(Race.self).value(forKey: "date") as! [Date]).sorted().map { (date:Date) -> String in
    dateFormatter.string(from: date)
}
        
let distinctsDates = allRacesDatesStringFormated.reduce(into: [String]()) { (result, raceDate:String) in
    if result.index(of: raceDate) == nil {
        result.append(raceDate)
    }
} 

return distinctsDates

Mais avec ta méthode, j’arrive à ceci: (qui fonctionne aussi)

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/yyyy"

return Set((realm.objects(Race.self).value(forKey: "date") as! [Date]).map { (date:Date) -> String in
    dateFormatter.string(from: date)
}).sorted()

Donc, si je récapitule:

  • Le Set fonctionne avec du HashCode, qui permet de limiter les opérations pour savoir si une valeur est déjà dans le tableau ou non.
  • Le reduce fonctionne comme une boucle for, si ce n’est qu’on utilise une closure pour “l’intérieur de la boucle”. Le premier paramètre de la closure est le résultat de la boucle au tour précédent.
  • Le map permet de retourner mon tableau en faisant un traitement sur les éléments de ce dernier.

Si ce que j’ai dis juste au dessus et correct, je suppose alors que la deuxième méthode (avec le Set) est plus efficace que le reduce? (niveau performance)

Merci pour vos aides, :smiley:

Alexandre

Merci @Draken pour ta remarque.

@Alexandre, à priori, avec ton Set, Map suivi de ton Sort, tel quel, tu n’auras toujours pas le résultat souhaité, je pense.
En effet, en finissant par le tri, tu tries une liste de chaîne de caractères et non des dates :confused: (ce qui peut engendrer des erreurs).
Si tu ne veux pas faire de reduce, le top est de faire un truc comme ça :

return Set((realm.objects(Race.self).value(forKey: "date") as! [Date]).map { (date:Date) -> Date in
    dateFormatter.date(from: dateFormatter.string(from: date))
}).sorted().map { (date:Date) -> String in
    dateFormatter.string(from: date)
}

Mais ça rajoute 2 traitements sur des dates et une map, je ne pense pas que ça soit plus intéressant que de passer par un reduce, au contraire :thinking:.

Je suis occupé de réglé le soucis que j’ai, je fais quelques essais pour vérifier que j’arrive bien à ce que j’ai besoin au final, et ensuite, je posterai ce que j’ai comme code, si vous voulez bien y jeter un oeil et me dire ce que vous en pensez :slight_smile:

Voilà, je suis enfin arrivé à ce que je souhaitais! :grin:

Tout d’abord, merci à tous ceux qui ont pris le temps de me répondre dans cette longue quête! :smile:

Donc, petit récapitulatif du résultat attendu:
J’avais en base de donnée des courses (Races) que je souhaitais afficher dans une TableView de la course la plus récente vers la plus ancienne. Je souhaitais également classer ces courses sous différentes sections (une section correspondant à une date).

Par exemple, si j’avais comme courses:

{ Race("race 1", date: "05-02-2018 11:40:32"), Race("race 2", date: "06-02-2018 11:40:32"), Race("race 3", date: "06-02-2018 15:40:32"), Race("race 4", date: "07-02-2018 11:40:32"), Race("race 5", date: "07-02-2018 17:42:18") }

Je souhaitais arriver à:

-- 07/02/2018 --
    Race 5
    Race 4
-- 06/02/2018 --
    Race 3
    Race 2
-- 05/02/2018 --
    Race 1

Voici mon code source:
Je reste ouvert à toutes idées d’amélioration (performance, lisibilités, explications, etc :smiley:)

Code de ma TableView:

Dans mon TableViewController:

var _races:Results<Race>?
var _racesByDates:[[Race]]?

override func viewWillAppear(_ animated: Bool) {
    _races = RacesManager.getRaces()
        
    if let races = _races {
         _racesByDates = RacesManager.getRacesByDistinctsDates(races: races)
    }
}

Code de la TableView à proprement parlé:

extension MyTableViewController {
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        
        guard let numberOfDistinctRacesDates = _racesByDates?.count else {
            return 1
        }
        
        return numberOfDistinctRacesDates
    }
    
    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        if let date = _racesByDates?[section][0].date {
            
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "dd/MM/yyyy"
            
            return dateFormatter.string(from: date)
        }
        return nil
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if let distinctRaceDateAtRow = _racesByDates?[section] {
            return distinctRaceDateAtRow.count
        }
        
        return 0
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let race:Race = _racesByDates?[indexPath.section][indexPath.row],
            let missionRace = race.getMission(),
            let missionLabel = missionRace.getLabel() else {
            return UITableViewCell()
        }
        
        let title = "\(race.label)"
        let subtitle = "\(race.date)"
        
        if let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as? MyCellModel {
            cell.commonInit(title: title, subtitle: subtitle)
            return cell
        }
        
        return UITableViewCell()
    }
}

Le code de mon RaceManager (où se trouve la gestion des divisions des courses en fonction des dates):

static func getRacesByDistinctsDates(races:Results<Race>) -> [[Race]]? {
    
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "dd/MM/yyyy"
    
    // Récupère un tableau de String qui contient les dates de manière unique triée par ordre décroissant
    let distinctsDates:[String] = Set((races.value(forKey: "date") as! [Date]).map { (date:Date) -> String in
        dateFormatter.string(from: date)
    }).sorted(by: >)
    
    var racesByDistinctsDates:[[Race]] = []
    
    // Parcourt des dates uniques
    for date in distinctsDates {
        
        // Ajoute les bonnes courses aux bonnes dates dans le tableau des courses à retourner
        let distinctsRaces = races.reduce(into: [Race]()) { (result, race:Race) in
            
            let raceDate = dateFormatter.string(from: race.date)
            if raceDate == date {
                result.append(race)
            }
        }
        
        racesByDistinctsDates.append(distinctsRaces)
    }
    
    return racesByDistinctsDates
}

Dernière petite question:
Pour le moment, j’utilise

sorted(by: >)

pour trier mes dates (sous forme de String) dans un ordre décroissant (date la plus récente en haut, la plus ancienne en bas).
Cela fonctionne, mais je ne suis pas sur que cela fonctionnera tout le temps.
Votre avis ?

Voilà voilà, si vous avez des remarques, ou autres, n’hésitez pas :smiley:

Encore un grand merci :grin:

Alexandre

Yeah… bin c’est cool alors :smile: !!

Pour ton interrogation sur le tri des dates en String au format “dd/MM/yyyy”, je pense que tu auras raison, que ça ne fonctionnera pas tout le temps.
Par exemple, si tu as des courses le 10/01/2018, je pense qu’elles seront affichées en 1er dans ta liste.

PS : Petit conseil que Maxime te dirait, étant donné que tu formates la date au format “dd/MM/yyyy” à 2 endroits différents (pour la même variable), tu ne peux pas la définir une variable de classe (ou extérieur aux classes) et l’appeler à chaque fois que tu souhaites afficher la date correctement ? (L’idéal, à mes yeux, serait de définir une variable uniquement “get” dans ton objet Race qui permet de récupérer la date au bon format. En plus d’éviter les erreurs de copier/coller comme dirait Maxime, ça allégera le code et le rendra encore + joli :wink: )

Merci pour ton retour @iMrMaximus :slight_smile:
Oui, je pensais effectivement faire de “dd/MM/yyyy” une constante pour ne pas devoir l’écrire à plusieurs endroits et risquer de me tromper.

Pas forcément, puisqu’il faut ensuite convertir le Set en tableau. Il faudrais faire des mesures de performances pour l’affirmer avec certitude. Mais bon, la différence ne doit pas être significative à moins de manipuler des dizaines de milliers de date.

Et tu devrais faire ton tri sur de véritables variables Date, pas des représentations textuelles stockées dans des chaînes de caractères. Le tri d’une Date se fait sur l’information réelle, alors qu’une String est triée d’après la valeur numérique du codage de chaque caractère.

1 « J'aime »

Merci pour ton retour @Draken :slight_smile:

Je vais faire des tests de performances à l’occasion, je suis curieux. :slight_smile:

Au niveau du tri, je vais voir pour le faire sur les dates directement plutôt que de le faire sur des String, ça serait effectivement plus logique.
Par contre, du coup, je ne peux plus utiliser le Set, si ? Puisque j’ai besoin des dates sous forme String pour que le Set fonctionne correctement (sans les secondes, minutes et heures). A moins que je convertisse ces dates en dates uniquement avec jour/mois/année, ce qui résoudrait aussi le problème.

Je me penche dessus et je reviens vers toi si je ne trouve pas. Merci :slight_smile:

Tiens, est-ce qu’il est possible de convertir une date vers une autre date (avec un autre format) mais sans la convertir en String ?

Le Set fonctionne très bien avec des dates sous forme native. Regarde cet exemple où je crée un tableau de dates aléatoires, pour les convertir en un Set, avant de le trier pour fabriquer un nouveau tableau [Date].

        let setDates = creerSetDatesAleatoires(nombreDate: 10)
        let triage = setDates.sorted()
        
        print ("---")
        print ("Affichage du Set de dates")
        print ("---")
        for set in setDates {
            print (set)
        }
        
        print ()
        print ("---")
        print ("Affichage des dates triées")
        print ("---")
        for date in triage {
            print (date)
        }

        
    }
    
    func creerSetDatesAleatoires(nombreDate:Int) -> Set<Date> {
        var tableau = [Date]()
        for _ in 1...nombreDate {
            let uneDate = creerDateAleatoire()
            tableau.append(uneDate)
        }
        return Set(tableau)
    }
    
    func creerDateAleatoire() -> Date {
        var date = Date()
        let nbJours:UInt32 = 60*60*24*30
        let delaiAleatoire = TimeInterval(arc4random_uniform(nbJours))
        date.addTimeInterval(delaiAleatoire)
        return date
    } 

Tests :


Affichage du Set de dates

2018-02-27 15:40:39 +0000
2018-02-24 02:51:29 +0000
2018-02-26 02:05:30 +0000
2018-03-06 09:20:56 +0000
2018-02-16 01:32:57 +0000
2018-02-18 15:58:48 +0000
2018-02-07 21:38:31 +0000
2018-02-26 04:40:51 +0000
2018-02-23 17:34:45 +0000
2018-03-02 06:09:15 +0000


Affichage des dates triées

2018-02-07 21:38:31 +0000
2018-02-16 01:32:57 +0000
2018-02-18 15:58:48 +0000
2018-02-23 17:34:45 +0000
2018-02-24 02:51:29 +0000
2018-02-26 02:05:30 +0000
2018-02-26 04:40:51 +0000
2018-02-27 15:40:39 +0000
2018-03-02 06:09:15 +0000
2018-03-06 09:20:56 +0000

Oui, le Set fonctionne bien avec des dates natives, mais par exemple, si j’ai ça comme dates:

2018-02-23 04:40:51 +0000
2018-02-07 21:38:31 +0000
2018-03-02 06:09:15 +0000
2018-02-23 17:34:45 +0000

Le set, ensuite le tri, ça va me donner:

2018-02-07 21:38:31 +0000
2018-02-23 04:40:51 +0000 // Cette date a le même dd/MM/yyyy que la suivante
2018-02-23 17:34:45 +0000 // Cette date a le même dd/MM/yyyy que la précédente
2018-03-02 06:09:15 +0000

Alors que j’aimerai qu’il me retourne:

2018-02-07
2018-02-23
2018-03-02

Pour que je puisse les utiliser dans mes sections de table view.

Puisque dans mon application, j’essaye de trier les courses de l’utilisateur, et que si l’utilisateur a fait 3 courses le même jour, alors, ça lui affiche dans la même section dans la table view.

Tu vois ce que je veux dire ?

Oui, je vois très bien. C’est normal parce que j’ai généré des dates aléatoirement. Le nom de Date est trompeur. En fait c’est un instant dans le temps, précis à la seconde prés, et non une « date humaine » du genre « 7 Février 2018".

En générant les dates à partir de tes chaînes de caractères tu auras des « dates » simplifiées commençant à minuit 0h00.

J’ai une idée sur la manière de résoudre ton problème avec des dates de n’importe quel type, je regarde ça ce soir.

Problème résolu. Je n’ai pas le temps de poster les explications et le code maintenant. Je fais ça ce soir.

----------------------
Il y a  5  jours dans le tableau
---
10 février 2018
  Se brosser les dents
---
11 février 2018
  Se brosser les dents
  Acheter les cadeaux de Noël
---
12 février 2018
  Enregistrer les Feux de l'Amour
  Se brosser les dents
  Manger des pommes
  Enregistrer les Feux de l'Amour
---
13 février 2018
  Acheter un billet de train
---
14 février 2018
  Aller chez le Coiffeur
  Se brosser les dents