30. Práce s úhly v CombiScriptu: od Pi po Atan2

Ilustrační obrázek k článku: Práce s úhly v CombiScriptu: od Pi po Atan2
Ukážeme, jak CombiScript sjednocuje práci s úhly mezi PHP a JS. Implementujeme goniometrické (sin, cos), cyklometrické (arcsin, arccos) i převodní funkce – a vysvětlíme, proč atan2 má parametry ($y, $x).

Prvním krokem při práci s úhly je mít po ruce přesnou hodnotu čísla π (Pi). V CombiScriptu ji získáte jednoduše – stačí zavolat funkci nm_Pi().

  1. function nm_Pi()
  2. {
  3. 0&&0<!--$_;/*
  4. return Math.PI;
  5. /*/
  6. return Pi();
  7. //*/
  8. }

Než se pustíme do detailů, shrňme si, jak jsem celou práci s úhly v CombiScriptu rozdělil. Pro přehlednost a logiku jsem zvolil tři základní skupiny funkcí:

A jako bonus – funkci atan2, která řeší, co obyčejný tangens nezvládne: převod kartézských souřadnic na úhel včetně správného kvadrantu.

Převodní funkce: radiány ↔ stupně

Nemůžeme zapomenout na převody mezi radiány a stupni. Zatímco PHP má vestavěné funkce rad2deg() a deg2rad(), JavaScriptu si je musíme napsat sami – což v CombiScriptu elegantně vyřešíme.

  1. function nm_Rad2Deg($rad)
  2. {
  3. if (!nm_IsNumber($rad)) throw new cs_Error('nm_Rad2Deg-$rad-NotNumber', [cs_Type($rad)]);
  4. 0&&0<!--$_;/*
  5. return $rad * (180/Math.PI);
  6. /*/
  7. return rad2deg($rad);
  8. //*/
  9. }

A samozřejmě i opačný převod – stupně na radiány.

  1. function nm_Deg2Rad($deg)
  2. {
  3. if (!nm_IsNumber($deg)) throw new cs_Error('nm_Deg2Rad-$deg-NotNumber', [cs_Type($deg)]);
  4. 0&&0<!--$_;/*
  5. return $deg/180*Math.PI;
  6. /*/
  7. return deg2rad($deg);
  8. //*/
  9. }

Goniometrické funkce: úhel → poměr

Teď přichází na řadu „klasika“ – goniometrické funkce. Začneme s sinem. Jako parametr jsem zvolil radiány – je to mezinárodní standard, který dodržuje většina jazyků. (I když já osobně mám pořád slabost pro stupně :-)

  1. function nm_Sin($rad)
  2. {
  3. if (!nm_IsNumber($rad)) throw new cs_Error('nm_Sin-$rad-NotNumber', [cs_Type($rad)]);
  4. 0&&0<!--$_;/*
  5. return Math.sin($rad);
  6. /*/
  7. return sin($rad);
  8. //*/
  9. }

Pokračujeme kosinem.

  1. function nm_Cos($rad)
  2. {
  3. if (!nm_IsNumber($rad)) throw new cs_Error('nm_Cos-$rad-NotNumber', [cs_Type($rad)]);
  4. 0&&0<!--$_;/*
  5. return Math.cos($rad);
  6. /*/
  7. return cos($rad);
  8. //*/
  9. }

A nesmí chybět tangens.

  1. function nm_Tan($rad)
  2. {
  3. if (!nm_IsNumber($rad)) throw new cs_Error('nm_Tan-$rad-NotNumber', [cs_Type($rad)]);
  4. 0&&0<!--$_;/*
  5. return Math.tan($rad);
  6. /*/
  7. return tan($rad);
  8. //*/
  9. }

Cyklometrické funkce: poměr → úhel

Samozřejmě budeme potřebovat i funkce inverzní – tedy cyklometrické. Ty z poměru (čísla) počítají zpět úhel.

U těchto funkcí se musíme rozhodnout, zda zda budeme kontrolovat, že vstupní hodnota leží v povoleném rozsahu (tj. -1 až 1 včetně) a pokud ne tak vyvoláme výjimku, či zda v takovém případě prostě tiše vrátíme NaN. Toto je naprosto klíčové architektoniclé rozhodnutí, které ovlivní soustu funkcí Combiscriptu.. Je tak klíčové, že ho zatím neuděláme a povedeme to jen jako technologický dluh.

Cyklometrické funkce nejsou jednoznačné – jeden vstup může mít více správných výstupů (např. arcsin(-1) = -π/2, ale také 3π/2). Aby naše knihovna fungovala konzistentně, musíme ověřit v testech, že oba jazyky vrací stejnou hodnotu (obvykle v rozsahu <-π/2, π/2>). Pokud ne, upravíme výsledek přičtením nebo odečtením 2π. Testy jsou tady další technologický dluh, který vytváříme.

První z nich je arkus sinus.

  1. function nm_ASin($num)
  2. {
  3. if (!nm_IsNumber($num)) throw new cs_Error('nm_ASin-$num-NotNumber', [cs_Type($num)]);
  4. 0&&0<!--$_;/*
  5. return Math.asin($num);
  6. /*/
  7. return asin($num);
  8. //*/
  9. }

Dále následuje arkus kosinus.

  1. function nm_ACos($num)
  2. {
  3. if (!nm_IsNumber($num)) throw new cs_Error('nm_ACos-$num-NotNumber', [cs_Type($num)]);
  4. 0&&0<!--$_;/*
  5. return Math.acos($num);
  6. /*/
  7. return acos($num);
  8. //*/
  9. }

A jako poslední z této trojice arkus tangens.

  1. function nm_ATan($num)
  2. {
  3. if (!nm_IsNumber($num)) throw new cs_Error('nm_ATan-$num-NotNumber', [cs_Type($num)]);
  4. 0&&0<!--$_;/*
  5. return Math.atan($num);
  6. /*/
  7. return atan($num);
  8. //*/
  9. }

atan2: když nestačí jen tangens

Pokud potřebujete převést kartézské souřadnice na polární úhel, nestačí vám obyčejný nm_ATan. Problém? Tangens neumí rozhodnout, ve kterém kvadrantu se bod nachází – stejný poměr y/x může odpovídat dvěma různým úhlům.

Řešení? Funkce ATan2. Ta jako parametry bere přímo obě souřadnice – a správně určí kvadrant.

  1. function nm_ATan2($y, $x)
  2. {
  3. if (!nm_IsNumber($y)) throw new cs_Error('nm_ATan2-$y-NotNumber', [cs_Type($y)]);
  4. if (!nm_IsNumber($x)) throw new cs_Error('nm_ATan2-$x-NotNumber', [cs_Type($x)]);
  5. 0&&0<!--$_;/*
  6. return Math.atan2($y, $x);
  7. /*/
  8. return atan2($y, $x);
  9. //*/
  10. }

Parametry uvádíme v pořadí ($y, $x) – přesně tak, jak to zavedl Fortran v 60. letech. (Ano, v prvním manuálu z roku 1956 funkce atan2 ještě neexistovala – takže pokud si ho pamatujete, gratuluji k věku :-)

Toto pořadí není náhoda: vychází z logiky atan(y/x) – tedy protilehlá ku přilehlé. Většina programovacích jazyků se této konvence drží – a my také.

Experimenty s opačným pořadím (např. v Lotus 1-2-3 a některých tabulkových kalkulátorech) považuji za velmi matoucí. Pokud někdo potřebuje jiné pořadí, měl by zvolit jiný název funkce – třeba atan3(x, y). Prohazování parametrů při stejném názvu vytváří v programování jen zbytečný chaos.

Celá knihovna nm_Util nyní vypadá takto:

nm_Util.php.js
  1. <!-- --><?php /*
  2. "use strict";
  3. //*/
  4. function nm_IsNumber($num)
  5. {
  6. 0&&0<!--$_;/*
  7. return typeof $num === 'number'
  8. /*/
  9. return is_int($num) OR is_float($num);
  10. //*/
  11. }
  12. function nm_Abs($num)
  13. {
  14. if (!nm_IsNumber($num)) throw new cs_Error('nm_Abs-$num-NotNumber', [cs_Type($num)]);
  15. 0&&0<!--$_;/*
  16. return Math.abs($num);
  17. /*/
  18. return abs($num);
  19. //*/
  20. }
  21. function nm_Sign($num)
  22. {
  23. if (!nm_IsNumber($num)) throw new cs_Error('nm_Sign-$num-NotNumber', [cs_Type($num)]);
  24. 0&&0<!--$_;/*
  25. return Math.sign($num);
  26. /*/
  27. return ($num > 0) - ($num < 0);
  28. //*/
  29. }
  30. function nm_IsAsInt($num)
  31. {
  32. 0&&0<!--$_;/*
  33. return Number.isInteger($num);
  34. /*/
  35. if (is_int($num)) return true;
  36. return (is_float($num) && floor($num) === $num);
  37. //*/
  38. }
  39. function nm_Pi()
  40. {
  41. 0&&0<!--$_;/*
  42. return Math.PI;
  43. /*/
  44. return Pi();
  45. //*/
  46. }
  47. function nm_Rad2Deg($rad)
  48. {
  49. if (!nm_IsNumber($rad)) throw new cs_Error('nm_Rad2Deg-$rad-NotNumber', [cs_Type($rad)]);
  50. 0&&0<!--$_;/*
  51. return $rad * (180/Math.PI);
  52. /*/
  53. return rad2deg($rad);
  54. //*/
  55. }
  56. function nm_Deg2Rad($deg)
  57. {
  58. if (!nm_IsNumber($deg)) throw new cs_Error('nm_Deg2Rad-$deg-NotNumber', [cs_Type($deg)]);
  59. 0&&0<!--$_;/*
  60. return $deg/180*Math.PI;
  61. /*/
  62. return deg2rad($deg);
  63. //*/
  64. }
  65. function nm_Sin($rad)
  66. {
  67. if (!nm_IsNumber($rad)) throw new cs_Error('nm_Sin-$rad-NotNumber', [cs_Type($rad)]);
  68. 0&&0<!--$_;/*
  69. return Math.sin($rad);
  70. /*/
  71. return sin($rad);
  72. //*/
  73. }
  74. function nm_Cos($rad)
  75. {
  76. if (!nm_IsNumber($rad)) throw new cs_Error('nm_Cos-$rad-NotNumber', [cs_Type($rad)]);
  77. 0&&0<!--$_;/*
  78. return Math.cos($rad);
  79. /*/
  80. return cos($rad);
  81. //*/
  82. }
  83. function nm_Tan($rad)
  84. {
  85. if (!nm_IsNumber($rad)) throw new cs_Error('nm_Tan-$rad-NotNumber', [cs_Type($rad)]);
  86. 0&&0<!--$_;/*
  87. return Math.tan($rad);
  88. /*/
  89. return tan($rad);
  90. //*/
  91. }
  92. function nm_ASin($num)
  93. {
  94. if (!nm_IsNumber($num)) throw new cs_Error('nm_ASin-$num-NotNumber', [cs_Type($num)]);
  95. 0&&0<!--$_;/*
  96. return Math.asin($num);
  97. /*/
  98. return asin($num);
  99. //*/
  100. }
  101. function nm_ACos($num)
  102. {
  103. if (!nm_IsNumber($num)) throw new cs_Error('nm_ACos-$num-NotNumber', [cs_Type($num)]);
  104. 0&&0<!--$_;/*
  105. return Math.acos($num);
  106. /*/
  107. return acos($num);
  108. //*/
  109. }
  110. function nm_ATan($num)
  111. {
  112. if (!nm_IsNumber($num)) throw new cs_Error('nm_ATan-$num-NotNumber', [cs_Type($num)]);
  113. 0&&0<!--$_;/*
  114. return Math.atan($num);
  115. /*/
  116. return atan($num);
  117. //*/
  118. }
  119. function nm_ATan2($y, $x)
  120. {
  121. if (!nm_IsNumber($y)) throw new cs_Error('nm_ATan2-$y-NotNumber', [cs_Type($y)]);
  122. if (!nm_IsNumber($x)) throw new cs_Error('nm_ATan2-$x-NotNumber', [cs_Type($x)]);
  123. 0&&0<!--$_;/*
  124. return Math.atan2($y, $x);
  125. /*/
  126. return atan2($y, $x);
  127. //*/
  128. }

V tomto článku jsme sice vytvořili podporu pro práci s úhly, ale udělali jsme dva technologické dluhy. Jednak rozhodnutí co s matematicky nesmyslnými vstupy a pak jsme se opakovaně dotkli klíčového slova: testy. Bez nich bychom neměli jistotu, že naše funkce vrací konzistentní výsledky napříč jazyky – třeba že nm_ASin(-1) nevrací v PHP -π/2 a v JS 3π/2, i když obě hodnoty jsou matematicky správné. Tuto jistotu ale chceme mít neustále. A proto se v příštím díle podrobně podíváme na automatizované testování našich knihoven.

Předchozí   Následující