Pomoc - Szukaj - Użytkownicy - Kalendarz
Pełna wersja: [PHP][regexp]Dzielenie wyrażeń arytmetycznych za pomocą wyrażeń regularnych
Forum PHP.pl > Forum > Przedszkole
shinuexx
Witam.
Chciałem sobie zrobić prosty kalkulator który będzie pobierał od użytkownika wyrażenie arytmetyczne i zwracał jego wynik. Np:
Podaję:
Kod
(10+9)/(11*8)

Otrzymuję
Kod
0.215909090

Myślałem nad wykorzystaniem do tego wyrażeń regularnych, ale pojawia się pewien problem. Otóż nie bardzo wiem jak wymusić na kwantyfikatorach, aby dzielił mi wyrażenie tylko względem najwyższego znaku tzn. powyższe wyrażenie powinno mi podzielić na:
Kod
l=(10+9);
r=(11*8);
sign=/;

oczywiście powyższe wyrażenia mogą wyglądać także inaczej. Np można używać na nich prostych funkcji:
Kod
sin(10+4)/cos(8-11)
l=sin(10+4);
sign=/;
r=cos(8-11);

Do tej pory używałem wyrażenia regularnego:
Kod
/^(?<l>[[:print:]]+)(?<sign>[\*\/\-\+]{1})(?<r>[[:print:]]+)/

Każdą ze stron wyrażenia arytmetycznego będę rozpoznawał rekurencyjnie, i do analizy funkcji mam już wyrażenie regularne
Jak można by to najrozsądniej rozwiązać?
Crozin
Wyrażenia regularne kompletnie nie nadają się do czegoś takiego, tutaj powinieneś wykorzystać normalny parser wyrażeń matematycznych (taki który najpierw rozbija ciąg na tokeny, a następnie tworzy z nich drzewo wyrażeń):
Kod
sin(10+4)/cos(8-11)

Tokeny:
sin, (, 10, +, 4, ), /, cos, (, 8, -, 11, )

Drzewo wyrażeń:

     "/"
   /      \
sin      cos
  |        |
10 + 4   8 - 11
Dzięki temu będziesz mieć możliwość wyłapywania błędów składni, ustalania pierwszeństwa operatorów itd.
shinuexx
Czyli analizuję ciąg od początku, sprawdzam co mam a potem czego oczekuję następnie. Jeśli tak nie jest to błąd a jeśli jest to następny znak?
Crozin
Pogoogleaj sobie za "writing parser c / java" czy coś w ten deseń. Jest masa materiałów, całość nie jest trudna (szczególnie gdy mówimy o wyrażeniach matematycznych) ale dosyć obszerna.
Ewentualnie jak z czymś konkretnym będziesz mieć problem, pytaj.
shinuexx
W sumie to nie bardzo już teraz wiem co miałem znaleźć. O LEXER'ach i PARSER'ach??

ok. Dowiedziałem się od znajomego, że można to zrobić za pomocą odwrotnej notacji polskiej. Jak dla mnie jest to naprawdę przyjemny i przejrzysty algorytm liczenia wyrażeń matematycznych uwzględniający pierwszeństwo. Wyrażeń regularnych użyłem do dzielenia ciągu na tokeny.
  1. public function _lexer(){
  2. if(!preg_match_all("/([[:digit:]]+[e|E]{1}[\+\-]{0,1}[[:digit:]]+|[A-Za-z]{1}[A-Za-z0-9\_]*|[[:digit:]]+[\.]{0,1}[[:digit:]]*|[\.]{1}[[:digit:]]+|[\!\(\)\+\-\^\/\*\[\]]{1})/",$this->_input,$m))
  3. return false;
  4. $this->_token=$m[0];
  5. return true;
  6. }

Nie miałem innego pomysłu na token'owanie a to było dość przyjemne. Jeśli ktoś miałby lepszy pomysł to nie mam nic lepszego.
Jeśli ktoś chce mogę zamieścić kody tego co do tej pory udało mi się zrobić tj:
-lexer,
-parser,
-counter,
wraz z obsługą podstawowych funkcji oraz podstawiania zmiennych zdefiniowanych przed wyliczeniem. Brak jeszcze obsługi błędów, co zamierzam wprowadzić.
Crozin
Cytat
Nie miałem innego pomysłu na token'owanie a to było dość przyjemne. Jeśli ktoś miałby lepszy pomysł to nie mam nic lepszego.
Generalnie użycie wyr. reg. nawet do wydzielenia tokenów jest błędne - bo one wyłapią jedynie pewne wzorce, a nie zwrócą rezultatu pełnej analizy ciągu (jeżeli coś nie będzie pasować to to ominą zamiast rozpoznać jako niewłaściwy token).
Nie mniej jednak jeżeli spełnia to swoje zadanie w zadowalającym stopniu możesz to tak zostawić.
shinuexx
No tak. Ale jeśli podczas dalszego parserowania wyrażenia znajdzie błąd (brak operatora itp.) to jestem w stanie znaleźć błąd występujący w składni.

Nawet jeśli miałbym coś innego użyć do tokenowania to co to miałoby być i jak miałbym tego użyćquestionmark.gif
Crozin
Kod
3 + #23
Przy pomocy wyrażeń otrzymasz 3, +, 23 podczas gdy powinieneś otrzymać błąd - wdarł się # przez który na dobrą sprawę nie wiadomo co tak na prawdę powinno się zrobić.
Cytat
Nawet jeśli miałbym coś innego użyć do tokenowania to co to miałoby być i jak miałbym tego użyć
Odczytujesz znak po znaku otrzymany ciąg, wyciągając z niego kolejne elementy. W sieci jest od groma różnego rodzaju parserów (bibliotek) więc łatwo można podejrzeć w jaki sposób napisać całość (pewnie nawet gotowca można znaleźć).
shinuexx
czyli
Cytat
Czyli analizuję ciąg od początku, sprawdzam co mam a potem czego oczekuję następnie. Jeśli tak nie jest to błąd a jeśli jest to następny znak?


Bo jak narazie stworzyłem coś takiego:
  1. <?php
  2.  
  3. /**
  4.  * @author ShInUeXx
  5.  * @copyright 2011
  6.  */
  7.  
  8. class math_exp{
  9.  
  10. /**
  11.   *
  12.   * Typy operatorów
  13.   *
  14.   */
  15. const T_POW="^";
  16. const T_MUL="*";
  17. const T_DIV="/";
  18. const T_ADD="+";
  19. const T_MIN="-";
  20. CONST T_LPAR="(";
  21. CONST T_RPAR=")";
  22. CONST T_FACT="!";
  23.  
  24. const I_NUM=0;
  25. const I_OPR=1;
  26. const I_FUN=2;
  27. const I_VAR=3;
  28.  
  29. private $_opr=array(
  30. self::T_POW,
  31. self::T_MUL,
  32. self::T_DIV,
  33. self::T_ADD,
  34. self::T_MIN,
  35. self::T_FACT,
  36. );
  37.  
  38. private $_allowed_function=array(
  39. "abs",
  40. "acos",
  41. "acosh",
  42. "asin",
  43. "asinh",
  44. "atan2",
  45. "atan",
  46. "atanh",
  47. "ceil",
  48. "cos",
  49. "cosh",
  50. "exp",
  51. "floor",
  52. "fmod",
  53. "log10",
  54. "ln",
  55. "rand",
  56. "round",
  57. "sin",
  58. "sinh",
  59. "sqrt",
  60. "tan",
  61. "tanh",
  62. );
  63.  
  64. private $_vars=array();
  65. private $_token=array();
  66. private $_input;
  67. private $_result=array();
  68. private $_output;
  69.  
  70. public function __construct($vars=array()){
  71. $this->_vars=$vars;
  72. }
  73.  
  74. public function addExp($str){
  75. $this->_input=$str;
  76. }
  77.  
  78. public function addVars($vars){
  79. $this->_vars=array_merge($this->_vars,$vars);
  80. }
  81.  
  82. private function _priority($n){
  83. switch($n){
  84. case self::T_MIN:return 0;break;
  85. case self::T_ADD:return 0;break;
  86. case self::T_DIV:return 1;break;
  87. case self::T_MUL:return 1;break;
  88. case self::T_POW:return 2;break;
  89. case self::T_FACT:return 3;break;
  90. case null:return -1;break;
  91. default:
  92. return getrandmax();
  93. break;
  94. }
  95. }
  96.  
  97. public function countExp($str=null){
  98. if($str===null)$str=$this->_input;
  99. $this->addExp($str);
  100. if(!$this->_lexer()) return false;
  101. $this->_reverse_polish_notation();
  102. $this->_count();
  103. return $this->_output;
  104. }
  105.  
  106. public function _lexer(){
  107. if(!preg_match_all("/([[:digit:]]+[e|E]{1}[\+\-]{0,1}[[:digit:]]+|[A-Za-z]{1}[A-Za-z0-9\_]*|[[:digit:]]+[\.]{0,1}[[:digit:]]*|[\.]{1}[[:digit:]]+|[\!\(\)\+\-\^\/\*\[\]]{1})/",$this->_input,$m))
  108. return false;
  109. $this->_token=$m[0];
  110. return true;
  111. }
  112.  
  113. public function _identify($token){
  114. if(is_numeric($token))
  115. return self::I_NUM;
  116. elseif(in_array($token,($this->_opr)))
  117. return self::I_OPR;
  118. elseif(in_array($token,$this->_allowed_function))
  119. return self::I_FUN;
  120. else
  121. return self::I_VAR;
  122. }
  123.  
  124. public function _reverse_polish_notation(){
  125. $return=&$this->_result;
  126. $token=$this->_token;
  127. $stack=array();
  128. foreach($token as $k=>$v){
  129. $pr=$this->_priority($v);
  130. if($v==self::T_LPAR){
  131. $stack[]=$v;
  132. $sel=1;
  133. }
  134. elseif($v==self::T_RPAR){
  135. $par=array_pop($stack);
  136. while($par!=self::T_LPAR){
  137. $return[]=$par;
  138. $par=array_pop($stack);
  139. }
  140. }
  141. elseif($this->_identify($v)==self::I_NUM||$this->_identify($v)==self::I_VAR){
  142. $return[]=$v;
  143. }
  144. elseif($this->_identify($v)==self::I_OPR||$this->_identify($v)==self::I_FUN){
  145. $last=array_pop($stack);
  146. $T=$this->_priority($last);
  147. if($pr>$T){
  148. if($last!==null) {
  149. $stack[]=$last;
  150. };
  151. $stack[]=$v;
  152. }
  153. elseif($last==self::T_LPAR){
  154. if($v=='-'&&($k==0||$token[$k-1]==self::T_LPAR))$return[]="0";
  155. $stack[]=$last;
  156. $stack[]=$v;
  157. }
  158. else{
  159. if($last!==null&&$last!=self::T_LPAR) $return[]=$last;
  160. $stack[]=$v;
  161. }
  162. }
  163. }
  164. while(!empty($stack))
  165. $return[]=array_pop($stack);
  166.  
  167. }
  168.  
  169. public function _count(){
  170. $vars=$this->_vars;
  171. $rpn=$this->_result;
  172. $stack=array();
  173. foreach($rpn as $v){
  174. switch($this->_identify($v)){
  175. case 0:
  176. $stack[]=$v;
  177. break;
  178. case 1:
  179. switch($v){
  180. case "*":
  181. $b=array_pop($stack);
  182. $a=array_pop($stack);
  183. $stack[]=$a*$b;
  184. break;
  185. case "^":
  186. $b=array_pop($stack);
  187. $a=array_pop($stack);
  188. $stack[]=pow($a,$b);
  189. break;
  190. case "/":
  191. $b=array_pop($stack);
  192. $a=array_pop($stack);
  193. $stack[]=$a/$b;
  194. break;
  195. case "+":
  196. $b=array_pop($stack);
  197. $a=array_pop($stack);
  198. $stack[]=$a+$b;
  199. break;
  200. case "-":
  201. $b=array_pop($stack);
  202. $a=0;
  203. if($this->_identify(end($stack))==0)
  204. $a=array_pop($stack);
  205. $stack[]=$a-$b;
  206. break;
  207. case "!":
  208. $a=array_pop($stack);
  209. $stack[]=factorial($a);
  210. break;
  211. default:
  212.  
  213. break;
  214. }
  215. break;
  216. case 2:
  217. $a=array_pop($stack);
  218. $stack[]=call_user_func($v,$a);
  219. break;
  220. case 3:
  221. $stack[]=$vars[$v];
  222. break;
  223. }
  224. }
  225. $this->_output=array_pop($stack);
  226. }
  227. }
  228.  
  229. ?>


i na większości wyrażeń działa (oczywiście jeszcze błędów nie obsługuje)
Crozin
Cytat
Czyli analizuję ciąg od początku, sprawdzam co mam a potem czego oczekuję następnie. Jeśli tak nie jest to błąd a jeśli jest to następny znak?
Mógłbyś już nawet na etapie wykrywania tokenów wywalać pewne błędy składni, np. ")" nie może się pojawić jeżeli nie ma poprzedzającego go "(", ale generalnie darowałbym sobie na tym etapie takie coś. Nie możesz przewidzieć wszystkich w tym miejscu, w dodatku "zabrudziłbyś" kod tokenizera.

1. Odczytujesz wszystkie tokeny, nie bacząc na ich poprawność względem gramatyki języka (tutaj: wyrażeń matematycznych).
2. Na podstawie odczytanych tokenów próbujesz utworzyć AST. Na tym etapie wyłapujesz błędy składni.

EDIT:
Nie analizowałem całości ale kilka błędów już rzuciło mi się w oczy:
1. "-" (minus) to nie tylko operator odejmowania (operator dwuargumentowy), ale i operator wskazujący na wartość przeciwną (ujemną) następującego po nim wyrażenia (operator jednoargumentowy), np.: $a = -$b;
2. W przypadku gdy będziesz miał zapis 2 + invalidFunction(6) w chwili obecnej parser potraktuje invalidFunction jako... zmienną. Nie muszę chyba tłumaczyć, że jest to bzdura.

EDIT:
Na prawdę polecam poświęcić ze dwie godz nad wynikami wyszukiwania c / java / php math parser (język jest tutaj bez znaczenia) - jest tego sporo
shinuexx
Co do minusa to podam RPN z przykładowych wyrażeń tworzy takie:
Kod
-3*(-6)
=
3 0 6 - * -
-----------------
(-3)*(-6)
=
0 3 - 0 6 - *
-----------------
(2-3)*(1-6)
=
2 3 - 1 6 - *
-----------------
-1-1
=
1 - 1 -

także myślę że z tym sobie poradzi.
Postaram się poczytać o parserach coś więcej.
To jest wersja lo-fi głównej zawartości. Aby zobaczyć pełną wersję z większą zawartością, obrazkami i formatowaniem proszę kliknij tutaj.
Invision Power Board © 2001-2025 Invision Power Services, Inc.