18
Sep/09
0

Calcul de distances et tests unitaires

Le système nécessite un calcul de distances approximatif entre 2 points. Pour cela, on récupère le calcul théorique sur internet (par exemple, ici).

La transformation en distance est une reprise tel que des algorithmes :

distance(Coord1, Coord2) ->
  {Lat1, Lon1} = Coord1,
  {Lat2, Lon2} = Coord2,
  AngularDist = math:acos(
                 math:sin(Lat1 * math:pi() / 180) * math:sin(Lat2 * math:pi() / 180) +
                 math:cos(Lat1 * math:pi() / 180) * math:cos(Lat2 * math:pi() / 180) *
                 math:cos((Lon2 - Lon1) * math:pi() / 180)),
  AngularDist * 6371. %% km

Les coordonnées sont passées pour l’instant sous la forme d’un tuple comprenand la latitude et la longitude.

Tests unitaires simples

Le but est d’être sûr de la conformité des calculs.

Pour cela, l’utilisation de tests unitaires techniques (ou micro-tests, de nombreux autres termes sont employés) sur les fonctions est complètement justifiée.

Le plus simple ici est d’utiliser diretement les fonctionnalités du langage Erlang, qui est basé sur des assertions (le terme = ne représentant pas une affectation comme on peut la connaître dans les autres langages).

Cette méthode a été présenté par le concepteur du langage lui même dans cet article à lire absolument. Nous utiliserons la même méthode.

On défini donc la méthode test/0 de la façon suivante :

export(distance/2, test/0)
 
test() ->
  170.2 = distance({53.1506, -1.8444}, {52.2047, 0.1406}).

on lance le shell, (via rake start pour ma part car je suis un gros fainéant et que j’ai accès directement aux modules de cette façon) et on lance le test :

1> geo:test().
** exception error: no match of right hand side value 170.19913935058239
     in function  geo:test/0

Le test échoue !

Ce qui est finalement correct car la fonction retroune la valeur exacte, alors que l’on désire obtenir une précision d’une seule décimale. On modifie donc la méthode pour intégrer ce point :

-export([distance/3, test/0]).
 
distance(Coord1, Coord2, Precision) ->
  {Lat1, Lon1} = Coord1,
  {Lat2, Lon2} = Coord2,
  AngularDist = math:acos(
                 math:sin(Lat1 * math:pi() / 180) * math:sin(Lat2 * math:pi() / 180) +
                 math:cos(Lat1 * math:pi() / 180) * math:cos(Lat2 * math:pi() / 180) *
                 math:cos((Lon2 - Lon1) * math:pi() / 180)),
  Multi = math:pow(10, Precision),
  round((AngularDist * 6371) * Multi) / Multi.
 
test() ->
  170.2 = distance({53.1506, -1.8444}, {52.2047, 0.1406}, 1).

On recompile et on teste à nouveau :

2> geo:test().
170.2

Ca fonctionne, puisque Erlang renvoie la dernière instruction de la fonction.

Il est temps de refactoriser tout cela.

Refactorisation

Un point important est de vérifier que notre arrondi est correct (pas trouvé de BIF pour cela). On le réalise non pas en testant la même fonction distance avec plusieurs précisions, mais en extrayant cette fonctionnalité et en la testant à part. En gros, lorsque l’on teste la fonction distance, on ne doit tester QUE la valeur de la distance et pas les effets de bord.

round_float(Float, Precision) ->
  Multi = math:pow(10, Precision),
  round(Float * Multi) / Multi.
 
distance(Coord1, Coord2, Precision) ->
  {Lat1, Lon1} = Coord1,
  {Lat2, Lon2} = Coord2,
  AngularDist = math:acos(
                 math:sin(Lat1 * math:pi() / 180) * math:sin(Lat2 * math:pi() / 180) +
                 math:cos(Lat1 * math:pi() / 180) * math:cos(Lat2 * math:pi() / 180) *
                 math:cos((Lon2 - Lon1) * math:pi() / 180)),
  round_float(AngularDist * 6371, Precision).
 
test() ->
  1.2 = round_float(1.22, 1),
  1.56 = round_float(1.55876, 2),
 
  170.2 = distance({53.1506, -1.8444}, {52.2047, 0.1406}, 1).

En termes de lisibilité et optimisation, il est également possible d’extraire la partie de conversion radiant :

convert(radiant, Deg) ->
  Deg * math:pi() / 180.
 
distance(Coord1, Coord2, Precision) ->
  {Lat1, Lon1} = Coord1,
  {Lat2, Lon2} = Coord2,
  RLat1 = convert(radiant, Lat1),
  RLat2 = convert(radiant, Lat2),
  AngularDist = math:acos(
                  math:sin(RLat1) * math:sin(RLat2) +
                  math:cos(RLat1) * math:cos(RLat2) *
                  math:cos(convert(radiant, Lon2 - Lon1))),
  round_float(AngularDist * 6371, Precision).

Et on relance la compilation et le test pour s’assurer que la distance n’a pas bougée.

En effectuant de petites étapes de cette façon on est assuré que le périmètre testé n’a pas subi d’effets de bord.

Enfin, on peut définir une macro permettant de ne pas calculer la constante de conversion à chaque fois :

-define(CONVERSION, math:pi() / 180).
 
convert(radiant, Deg) ->
  Deg * ?CONVERSION.
 
test() ->
  1.2 = round_float(1.22, 1),
  1.56 = round_float(1.55876, 2),
 
  170.2 = distance({53.1506, -1.8444}, {52.2047, 0.1406}, 1),
  ok.

Les calculs perdent en précision lorsque l’on stocke des valeurs numériques de cette façon, mais l’impact sur notre système peut être négliger.

On rajoute un ok à la fin de la méthode de test afin d’avoir une vue claire et un sentiment agréable lors de son exécution et que tout s’est bien passé.

Filed under: Erlang, Tests
Comments (0) Trackbacks (0)

No comments yet.

Leave a comment

No trackbacks yet.