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 π (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í navíc kontrolujeme, zda vstupní hodnota leží v povoleném rozsahu.

První z nich je arkus sinus.

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 hlavní hodnotu (obvykle v rozsahu <-π/2, π/2>). Pokud ne, upravíme výsledek přičtením nebo odečtením 2π.

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

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. if ($num>1 || $num<-1) throw new cs_Error('nm_ACos-$num-OutOfRange');
  5. 0&&0<!--$_;/*
  6. return Math.acos($num);
  7. /*/
  8. return acos($num);
  9. //*/
  10. }

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. if ($num>1 || $num<-1) throw new cs_Error('nm_ASin-$num-OutOfRange');
  96. 0&&0<!--$_;/*
  97. return Math.asin($num);
  98. /*/
  99. return asin($num);
  100. //*/
  101. }
  102. function nm_ACos($num)
  103. {
  104. if (!nm_IsNumber($num)) throw new cs_Error('nm_ACos-$num-NotNumber', [cs_Type($num)]);
  105. if ($num>1 || $num<-1) throw new cs_Error('nm_ACos-$num-OutOfRange');
  106. 0&&0<!--$_;/*
  107. return Math.acos($num);
  108. /*/
  109. return acos($num);
  110. //*/
  111. }
  112. function nm_ATan($num)
  113. {
  114. if (!nm_IsNumber($num)) throw new cs_Error('nm_ATan-$num-NotNumber', [cs_Type($num)]);
  115. 0&&0<!--$_;/*
  116. return Math.atan($num);
  117. /*/
  118. return atan($num);
  119. //*/
  120. }
  121. function nm_ATan2($y, $x)
  122. {
  123. if (!nm_IsNumber($y)) throw new cs_Error('nm_ATan2-$y-NotNumber', [cs_Type($y)]);
  124. if (!nm_IsNumber($x)) throw new cs_Error('nm_ATan2-$x-NotNumber', [cs_Type($x)]);
  125. 0&&0<!--$_;/*
  126. return Math.atan2($y, $x);
  127. /*/
  128. return atan2($y, $x);
  129. //*/
  130. }

V tomto článku 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í