-
Notifications
You must be signed in to change notification settings - Fork 22
Description
J'ai évalué le comportement des fonctions de tri disponible dans 4D au niveau des Collections, des EntitySelections et via la clause "order by" des queries, à savoir :
• Collection.sort()
• Collection.orderBy()
• EntitySelection.query("... order by ...")
• EntitySelection.orderBy()
Je me suis intéressé au cas du tri hétérogène (c.-à-d. avec des valeurs de types différents). Ce cas est mentionné dans la documentation à 3 reprises, toutes sur la page listant les fonctions membres pour le type Collection:
• .sort() https://developer.4d.com/docs/API/CollectionClass#sort
• .orderBy() https://developer.4d.com/docs/API/CollectionClass#orderby
• .multiSort() https://developer.4d.com/docs/API/CollectionClass#multisort
La page de doc des fonctions membres des entity selections (https://developer.4d.com/docs/API/EntitySelectionClass) n'en fait pas mention. On retrouve des redirections vers la page de doc de la fonction Collection.sort() dans les paragraphes de description des fonctions EntitySelection.min() et EntitySelection.max().
La doc indique que le tri s'effectue en groupant d'abord les valeurs par type, puis au sein de chaque groupe de valeurs homogènes. Le tri ordonnera les types comme suit :
- null
- booléens (false < true)
- strings (par ordre lexicographique : "a" < "ab" < "b")
- nombres (du plus petit au plus grand)
- objets (aucun ordre particulier)
- collections (aucun ordre particulier)
- dates (du plus ancien au plus récent)
Trois questions me viennent à l'esprit :
A. il existe d'autres types que ceux cités et qui peuvent être stockés dans une collection, que se passe-t-il lors du tri ?
- Time
- Blob (automatiquement converti en 4D.Blob)
- Picture
- Pointer
B. comment sont traitées les valeurs particulières du type Real (que sont +INF, -INF et NaN (not-a-number)) ? Cette dernière est particulièrement gênante car elle ne participe pas à la relation d'ordre naturel entre nombres (même le test d'égalité échoue : NaN == NaN renvoie False sur les CPUs), à moins d'enrichier le code avec des traitements particuliers.
C. existe-t-il une différence de comportement entre les fonctions de tri ?
Voici ce que j'ai observé sur les versions 4D main, 4D 21 (build 100225) et 4D 20R10 (build 100299) (je n'ai pas détecté de différences entre les versions) :
Dans une collection :
• Si placés dans une collection, les types Integer, Real et Time sont tous convertis en Number, et sont donc groupés ensemble durant le tri. Attention toutefois à la conversion Time vers Number : elle dépend du sélecteur SET DATABASE PARAMETER(Times inside objects; ...), pouvant être Times in seconds ou Times in milliseconds.
• Si placé dans une collection, le type Pointer est considéré comme:
o Object si non null (c'est à dire qu'il pointe sur quelque chose), et groupé avec les objets lors du tri.
o Null s'il ne pointe sur rien, et groupé avec les valeurs Null lors du tri.
• Si placé dans une collection, le type Picture est toujours considéré comme Object, et groupé avec les objets.
• Si placé dans une collection, le type Blob scalaire est converti en instance 4D.Blob, toujours non-null même si le blob scalaire est vide. Les blobs sont donc groupés avec les objets lors du tri.
• Si placées dans une collection, les valeurs +INF, -INF et NaN ne causent pas de problèmes lors du tri. La règle appliquée est la suivante: NaN < -INF < valeurs finies < +INF (NaN est le plus petit, suivi de -INF, suivi des valeurs numériques finies, et finissant par +INF qui est plus grand que toutes les valeurs).
o ce comportement est implémenté dans VReal::VValueInfo_real::CompareTwoPtrToDataWithOptions()
• Additionnellement, pour les objets et les collections placés dans une collection en vue d'être triés, 4D utilise une comparaison très peu coûteuse visant à donner une sortie "stable": des appels successifs à .sort() ou .orderBy() sur la même collection renverront le même résultat en sortie (c'est moins troublant pour l'utilisateur que de voir les éléments changer de place à chaque appel à .sort() ...). L'ordre est basé sur la position en mémoire des objets et des collections, c'est extrêmement peu coûteux à évaluer.
Dans une EntitySelection :
Nous avons un fonctionnement qui s'écarte un peu plus de ce qui est documenté, avec des limitations additionnelles et quelques fragilités :
• case manquant dans ObjAttKey::BuildFromJSONVal()), contrairement à ce que dit la doc. Ca semble être un bug.
• Si placé en tant que propriété d'un attribut objet, les types Integer, Real et Time sont tous convertis en Number comme décrit pour les collections.
• Si placé en tant que propriété d'un attribut objet, le type Picture et le type Blob sont traités et triés similairement au cas du stockage dans une collection.
• Si placé en tant que propriété d'un attribut objet, le type Pointeur donne la valeur Null, peut importe s'il pointait sur quelque chose de valide (les attributs de type objet ne peuvent pas stocker de pointeurs). Lors du tri, ces valeurs sont groupés avec les autres Null en premier (conformément à la doc).
• Si placés en tant que propriété d'un attribut objet, les valeurs +INF et -INF sont correctement groupées avec les Number et triées logiquement.
o elle sont bel et bien groupées avec les valeurs numériques lors du tri, mais leur emplacement au sein du groupe n'est pas prédictible,
o sur les versions debug de 4D sous macOS, la présence des valeurs NaN lors d'un tri peut causer un crash (cela est dû à une étape de checking additionnelle rajoutée par l'implémentation de std::sort fournie par la stdlib qu'utilise Xcode, qui vérifie au maximum si les 100 premiers éléments en sortie d'un tri sont bel et bien triés, et appelle abort() si le tri n'est pas correct)
o l'implémentation du tri pour ce cas utilise ObjAttKey::Compare(), qui ne traite pas les valeurs NaN comme dans VReal::VValueInfo_real::CompareTwoPtrToDataWithOptions().
o la valeur NaN est convertie en Null si stockée directement dans un attribut de type Number, et ne cause pas de souci lors du tri.
Au niveau du débuggeur et du DataExplorer :
• Le debugger 4D ainsi que le DataExplorer affichent "null" pour -INF, +INF et NaN si ces valeurs sont stockées en tant que propriétés d'objet, ce qui est déroutant (c'est un bug d'affichage au niveau du débugger, sans relation avec le tri):
Sur la vue suivante du DataExplorer, les trois entités montrées semblent contenir avoir un attribut de type objet ayant une propriété "a" valant "null", mais ce n'est pas le cas ! Les trois propriétés sont du type "Number", ayant les valeurs +INF, -INF et NaN respectivement.
Ce problème d'affichage n'est pas lié au tri. Il est dû au fait que le débugger utiliser des routines basées sur le comportement de JSON Stringify(), qui ne supporte pas les valeurs -INF, +INF et NaN et les affiche comme "null". Les débuggers Javascript utilisent des routines séparées pour ne pas réduire/masquer la visibilité sur les valeurs. Par exemple, avec NodeJS, le REPL ne stringifie pas avec JSON Stringify, et permet de voir la valeur réelle :
En résumé :
• le tri au niveau des Collection semble très bien implémenté (les valeurs NaN, -INF et +INF ne causent aucun problème)
• le tri au niveau des EntitySelection est fragile, on peut le renforcer/compléter et l'aligner sur celui des Collection,
• la documentation pourrait être améliorée en réduisant la répétition sur la page spécifique aux Collections et en améliorant ce qui est décrit sur la page relative aux EntitySelections.