diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000..08ca2c4 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,29 @@ +# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Java CI with Maven + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + cache: maven + - name: Verify and analyze with SonarCloud + run: mvn -B verify sonar:sonar -Dsonar.projectKey=ezylang_EvalEx -Dsonar.organization=ezylang -Dsonar.host.url=https://sonarcloud.io -Dsonar.coverage.jacoco.xmlReportPaths=/home/runner/work/EvalEx/EvalEx/target/site/jacoco/jacoco.xml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index c270cd4..f715a02 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,31 @@ -/bin -/.settings +# Eclipse .classpath .project +.settings/ + +# Intellij +.idea/ *.iml -.idea -target +*.iws + +# Mac +.DS_Store + +# Maven +log/ +target/ + +# Compiled class file +*.class + +# Log file +*.log + +# Package Files # +*.jar +*.war +*.nar +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f9dd7ea..0000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -language: java -sudo: false -install: true -jdk: openjdk11 -addons: - sonarcloud: - organization: uklimaschewski - token: "$SONAR_TOKEN" -script: - - mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent install sonar:sonar -Dsonar.host.url=https://sonarcloud.io - -Dsonar.projectKey=uklimaschewski_EvalEx -Pcoverage -cache: - directories: - - "$HOME/.m2/repository" -before_deploy: - - openssl aes-256-cbc -K $encrypted_044baa266d1a_key -iv $encrypted_044baa266d1a_iv - -in .travis/gpg.asc.enc -out .travis/gpg.asc -d -deploy: - - skip_cleanup: true - provider: script - script: bash ./deploy.sh - on: - branch: master - - skip_cleanup: true - provider: script - script: bash ./deploy.sh - on: - tags: true -env: - global: - - secure: jGr5mZsN7sYHwDfcHQ9BpSUkTRRTe9abIlMzz8xrBmO/Us+MioIKxRIp8CBiLW9/NGfAsuIt/74wirUzJbHkyVyH/rLV2BHB6vaTe+9IJpl36sPT//0DdAQH5I1iDlbTqkOMGkkjkVn5LwD98dA7AkrHr128lSmUplof1DXHiM0hdqRMseisHpyjO7g6wnkFX/gLQtkuMDIzSC0+ZGrHiVkbP97msgyiWCz2U1wf77D/TS2Ohgeis+7ygZc0OPM8qkyzylIgCQVfnOAoY/95RIXm3MH5HjfqJQSbz6cKX5IK+hiL8n5p3228MsIwOMlA4OLxFpHtW9dHi7h8ei0K+W/8a+V8XK1P/TP2RgaER3mWQ+lmEZ9Qyvbf3E7VVyawubE+l297357ty+FggEwc4gXS+1vRvb9dRLmkVuVov4lQWV2ZVqhhTXwUjL3tWPdlv78bxv453r6eX+shPZQtWMkcRjYgEFuntDcfBgIgagBpO1QBUiiIKYhsHx/KWp/PFR34a/QRXDBWcXtYBUzQG5i8S15c5+Ql40pgQFtw7RWUErENDndfccHEras5fx6VzkVYzaQDdRk4caeLzTTK/52cz/JCfVAT1bQ0rUGQoQ+vllUzbr6kUzjthur3RtbO/QhhemQsv7Jf+rFyzuhueoBCFylyH4m+DOIpTIv9VxU= - - secure: lgMcJkV+MFMryleJKt/5t96Ifane1JQ+DMnSbKm4uFVnjdOFuQm3DeL5FidBB+e2NsOpO1+ZQgWnYcNAphv6Gc1ToRBSXRVkQrIE9HXFDULKl1qdGaHFVgdW4Ypa9u8vcSHSRXeLtPz/uJn8Rm7H7QaFs2thaNusUgWaO3V8rbeoKDNE4vyDKptJmVw/9n2noEOrQPn4ctl/6wIoAW01E9pojhRyYeNqv1hIfMseG2sdLjJEXALa8jFKG4fgjXSUCaEp1wW7oLYhOyPkOeqjRYitAe5iOh22njh2vtacXyIiNvSI8q5PC5xZbq3MjxkSerxlnhx5jdCRdHlH7udZC/hQ9ouxkisZFjQyi+FRj4zGUPb2ohUCDyXiCL2+80Y6MESZk7GXiH3j9zbJ/hLTGd2fCsKMZHeLFhQXGEV5ppTPjVYSZ4MW615b0lRdZxb9j3rvwwrc7l79KRz27Xm426HqPwhMSy6f/hx828mnCcsN8p4Yq+cXR0fsXcLCt1tp0vPBYR8jA9osPlo9Uno2PN+ax1LXvCC9srm5EfOPjPs0DfWlLU0oNm8+MUx1MgDi2vMFG7LUOwDY/vwVSfG9hyDQ3Wddm+uD0CmwJu89ndPnAvMhtKCWaNRAAsVOgumExlTM/ksfftFIStKm32q8DpK9DGLAK+/78yXGciJb8KQ= - - secure: JUXh2wRIb2Q6R7ewYDs3wWr2io+bn71sfs98csYP4Nj7I5wtDFFPivsAE1BnTHOhSXWQi3S7+Nwx9104y+RM5z2eat5gDvr7NngLD3/MDWfxeuzuzh956myOjn3rbrDV5MAGcFL6P8ELyv9LmLTiGmg5kT8Ae7VpuMXQ/zkvkbMv4kR09gCnWq7ZsG1SphaI14Sug5GGQdP3zyeyOYVOQrZGCTuDnEs7eHphz+nqxtWISS5bG/fPZys2uitsiCv0qRF0JvxP3gJYmTVlUVxs9cWzHm8p/Lr1d9YtYN291dg85yUVv9NvPveEmbVlauPpQwUGQbIodLTXErhUwHBdwObFRJMikNyLXWZ7P4Nbfc6Mb3QqMl9g2aWapJNUnp8VjjOxuJ21HBrnN2nEKozjPv01U2ugSfz/U4V+cp3lxBFquMzbg7FW3pMIaxCsUeeE75nYUNYn+tM3rajtG8M05yiFBRFBY3GmsXKGe/tNaQzC4d4it3iTP3VP+8Fmz1VsU4YBtRX3TwrQZeGUt1+LyRX3qtWXXla692juo59yylPAMGr1iE+y4NWi2lfcLRqOET2ZpYYtHJ693wHhY2HUCaLilSYMTsznQIAUMq7bU6/91oisz4+1FRfC0EMByMbWEYq/XFEbD8zbr5XR6xxdCCPOxiAPhavgsbFrLUUaYms= - - secure: Eo8k7rucRejZHS/LHrjCDzOFUUkHYZOMQki6/9BV6LVPAVBTm30+ScS5T/tfkZ85eIjqEaC8GdNuekTfkbrHwqt6L8qcUW3K3HaDUUKG+ROVhsoL5L/mp42BUkdw5WoL6FEL4ziJxHtOWigMg5y984sGz06eID0NG5R2iUHYUcZRvwJ1E0DJtqqwWy3p69uhrtDkidkXw73VDT/Ya9ktLpSecwS7s9Y1eMFZ5eTc+nsAyHh0UipFXHDkm/tqw2MxWdDQGcgohKN/4EZcoe9G5DM1y9G6q9YWEjX8aQiFkC4vKuz42tC/3l0WLB3xW3O3L9xyvMV5CSGhLFC0brgJtRfZpum+hgQwF6VBBMx8MYXfdU2FsmWb61wLT5UfBSb0SupJ5Ysm11DR+oA2fI4LRmryvzHIxWHUaIxkrYdh9593HLwEuPVIP15sxfemXnwejdzId8ir8apLTDvgAXanWsQsyfDbBkBR6o9yCVUeiLJlHA4oO305KViQyo3H1mNo3bCGT37As+6Jn9Ewz7MbDv+sPmQXBEZL3f9BNh2VinDYuNXWO7QdBHHWxVr+pD/3PeP2zyt2uL8+QF3Ql4G9Qwzz1KgB7lLnMW4RgOVoR8YU9rxQdgsqr9z6xcIlLiblqtUzFeIsuF/n0Cxpo0qDbdMMSoF/63AP6ZZBdvUSXRs= diff --git a/.travis/gpg.asc.enc b/.travis/gpg.asc.enc deleted file mode 100644 index e6e7faba3dd27904ee065286bf419e4d8dd1ad7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7696 zcmYeFK4t54wMzXq6SK*DX@_TCWQ*fyU-Bcb*YDsz?GwzJzl?h7`K`K=HiU0E(`=-) zt|NBBg3aCY8ZO5?eQ2%3ylMyI?MIRSWZWVR?B=TE&+7S=yIS(jT~+rpO}d;L1S^>? z_4S2m3Lh!ukr9}8-9hx43v)=W?y_eGcd*>=w|J^QpC{$f!E5$mmRn4kd{=$YQQxX5 zDtpNKh3FE+^z25VTh~Lfj_f(L`Lrw3sI{F>-(cgO-C{Nm z%RBaKIZQcl{EP85)+v_mSGaG@vwiAyJLIP3y0=p=?B5aD+_*43?)S@$hv#|R`ewSM zlWSw%@uFa}mG`yyF7a4ZBpc6)p5V9Y)_dXOt6U<_toc;+A;ozg5BGC{M+a65&9GXm zE@0YQ#9DWJ{srqFJ<@6wbG`0853m37-u_>RsL;MEi$9-kYGaA^OkHREeY#O$PSv)fj;UAf?Or_lmMYtabGzOD)-B|0Vh`J7t(=-#?xh)^eZ4R@ zHTHGwfjd>*ABZ@!QEIU$*SfGc{|=6j1tE^4Dl%*xJj+z27sQa8}Pc`%5n1 zPDQWB!i}eN)L;DPwh8$BUft5Nso{(M+06&lX?t%kz^W}a&^9W$>E4AENAwJQ&d3uu5?A)>1&iO+EqrVa5nWZZa8?e!F+ef z-5>nVZ*Ntr|7u&enE#^r`aQWC`YY^yY~5r%S#SCI zBz)VS)8+;deG^|^QS$ouKV*55Y$?xyj*$Cjs?@V>BehH;tUgWgiIV+mufD=pKYDt5 z;nT%2)uBl~oA#&{YfPFxZ~3g{*+wPDA6|Xmu6VNh`_i4uJN>ntKZZ<8<5h8;?4EIR z#)nM}o^O-(>AimHp0?NJ;H{6VTE(6`OAEA2xp<@?`RrYfMeBY(*)m_3<>^3;Ok)YHMcBkKp{_&f21a}Ig2N&JQ_%BeS;I?zgDTBiY>~^H; zD^0gs&D?Tek(hO7wse+5t9r_!_jTV&62tgv*8G!Y-E(76-^Uf{Gv6~?_|1}$S+mBX zruI(9+sHL1?sDs=q~>-iu(y6+9N5V494s?*{o=3mJ==9mN`vpR zJ2sB<%II^^nf_i9teDUL`1h2{mnD#BtYG zWP7M}zylY}KOV;acHIqC-EOz=&$B!6EU%0;x6Pa-cy@_HlbqcBInIUC8^XD^c)W6t zvfr`HhHFn%^s#7B#d8u%M8)@V-Z0;-H!<|%8ubv~#Di=uZ)3jqntZ)pud&+pC&TlQ zAkLR}bN(;ad1~gqFTjT};AQyE7k@f_aXnf&_m>L)r1?Q7b9@p{%ZZt6xgp5C>GhXK z$u0}^43duAJ!F1(pGle9+Zf$h2m6-Va`vv)e7)$&9lKweui9hWs?_-xeQS@J#MzSm zbcHPw^8~GA=6|0|o+(Og5${M}vRFr`oNcF|p}O*d?HVB#GE-;z=|&z&vY$}8#CU?N zGW+ArqU=t4Rjmxn{eEBg=~HJQX7+|*M{AyW_8PUgiHF|IxqMUJc!7M`8ijgUri+u_ z>^jOecVl|mn}>%EtW?we60D-U;ldvqFQN0dy>Hc4m8I7=tXS-!QR6r7M}@I*`@ap9 zmu)|mxh9?0nNa2X^T347A1{lE7+4(+tgw30Gh@y#c^$6qMtPr)hx^uh2sxVTyDyox zokMWK>sKuWT1I*f>cne9g#9=%{%C>y3Z@z?sT-iF#e{;_tIzuYE99e(a1Y+9gu z>fF0mq34y46#1RMDNuehKDYV6rNaO3FC70<-l-bpE^2bSD~9jfO*Z!E%NKl4SopjY#8uD!epAk}1EKHE)+R@jTci^^fjy=9de!o1FSJn}?c$;cV@ISG(#MeLDYgZq7F6Jc?9{nID^>?+yes-Tp z&Y%CtnflkQjl5aO9DH!uJ+w&CpIyBJb_x7=y}pNwcp~U-oTUe0_dO!_?-l%RNh9Pn2S2Q2W;N(`fqC<7b7VAIer8 zk-x>jyj%9`yNBgZj<3vDZq-X?*rJwgmfK}IS4!74qx9oncJ`w^zb?#r{Z{hoJPF1F#+;js zEIE48C;B*Q9zWIXwIoyQ6|=icK1cg)S@(ZgkN^GmU@1|%()73EoJp6L*6Wb?1%^j& zt;#8}U(5HvV$$;6GS&*3flB<$3oh*!<>0-xIag!-7u|<{q_S$o#U=0iFP(EG_wdBo zFI*c+t50W$(dyE&p{{Cw5hhV>tnN_j3w0}kwc`i^oqt2yy z@U;i?S^Fh{Lc1Aalk$)FW~4u`*jBPIRA}NVyMJN!1_ILjf3{neJoB;9TPqT#zEW#P zX2*n++cY@*zhAqTE*haVH}!M_TkRFSvWDpTj}~4AP1pHFH>-uviIjzbnlxH%JptHC6SM{`v zs^Y18ET>hLDE*$tnOv>H!#JO_#A<`&`KD@U$u=jE5d`1 z2#03pR)1P9#{Kerw|;ZgEWwZGUuD_%z1&v3dWJO%e}m5QdEyGw=Sn-iZk;#jiOp8V zg*yui+&?W1Wm?qoM0>A=*qsfFA}@G}9aU^ge!ulebxV@rMDQog}T z^8K941^C15F**Ts4e;%|yD746L-RZtGwDO-=gOX+1 z2ugu#Hgq*@gam+(#w6h*_^LU2fU50{!V&-x^oFX>nmW8%!>o7bcnZ{}U-dKmxT$vCj6h~3F)X(Fm-h3&dolm+ z51VgodY>e+{c`;GA8enlXjs46d~iY`!yYE)^o|>kwl1nZo#Z`L&gs#ur%@SCmo9!7 z+Pj$FDd7Dc@%>k$D>fZ*{S#baTF$z(p7Cm>%)X|+l3#@=salImM!}IZmL6- z_|fyG@5I&}bJ}?7nq=0ljM9?^+zQMBQ$I#)Sc$ZDte<;hlEZ=&5C86`*XQ13Q#$yn zsc(wOce!1kKQsh1rttncx9!MXRU3|-ExJo4&u`#68z=04V_v-7JT{*1s=FBq*Ohxt zc)!Ni^V9EJdzS7xzdKsW|J3m_lV0C=mi%_gi6v=up&JAz9ISj5&=?_{J5#DeXRFrI zTP0c@mv7u--Q%say{zQ>)rqGUaYgL-RJNI!k?H=DE)F-I0H#ex1o-(M+`f4BU+^<4 z#W#yKD9SCj;y0{a`2MiO9=8YYF6MCuoAv$j{D0@5D~IQU=9OhmdC!04@SQN;UTra{ zyixFML+2g3-fVmHnBXKo`+}gvZ8Q1G>VH&g+>EVE6Ak`2u|)dhnhjH#lr{G&UrrQ0(<0~A zyDCF!vYZyvsn6;SJ7uq!HMb;B>2UBrZK`mlvg*~g8MThH*z`1}zm$G?`~1!|GvCHv zTh`f?HA`@w%=0Vr|8+0+xN~;xj{K0u-P6M#ubML_GBNtw8ue#BM-1;AyM6rC?G-Dw z%lQ^gGTT1oe$({%pB3-iTsaXqY z$P~($d(lF=T-osNDgIuT>Ow`^tNCZ5IY zhcpU!_@DOW{=H+8m0WU%m+QyuyNau}Go*f4E55T$P^wep?)o;7RR@+`(C|F@A*lbwj%BCjy^t6A zGix`CutJY~`ddDs-S%G&&6(I?wezp+hnC#c;gWAkie~19_w9Z7wQsh-*i0^zGu3<_{Py?Y#seyZq_baxQb7spCx-) zdfLKAN7qc7u9q$B%&aOEBY1Mw44vG*&84#g4kcx5GEuo9dDZyzw#U&sPItIHn5tem zGvVhBx0w9=mP>g`I~E`=$9xd3^exOcv zH{TVP++7b-UKq`fojv8GdcI$os#6N5hG@`=9C6ciekV@_sR~Rl+1v7|VatY>_nYqt z$%+5HJ@KjNvx@%?BSyR7|`*UQtt z+?@3x-#PirlR%bvEzTv;Pgd>e``Q`xW#a$x_y4b~>-OGv{)m@EX!@%2w}j6Xn63SB zdiOf%cb}K&Ul&Z6UpSlPwfx#A@!>DF$Xr_auHN^1_vDp(6yMA|E2+yd_3Y2`!qq`d zt8(Z2X3Z$R`1nLTw=FPz>U z81DRc+QdckW7##im>urBALWnU;`ckl;_Up@Pjp^ADL#EC_vGsg>l<+aS6P2;{%~fk zBv;978y;(eecw_Woc3SR6OW(mo}c5VIc;0<(j)H|{CrihOR->*b=}sl`#TlQ{>^o^ zS5&-XUZ2qYXY=aZir0VM%Cauttl?Z%w_3~T?7|;%pSm1$Ugi5plodp7XFcHhW9!<) zdENHE^0I%O^jKOMsdD7~uU5fzKb1~%zq+$;_J()9+-qk`xlVsL|535n{!{fb&IeWM z!WOTcUDV2YW)kX z>7idv#M}JpaLU`VZj<7r_m>Zw?WrqSdFSumIHL#k7e4<#xz8zUN#&d-mU|NV>`(j> z|8}pP#(a`N^-jTmMw2inM}_?j;Xj#jI_@4n&Lrz^vGmPpU)}j9ukJNFu60dlHS-?f zhn;_Jrl!7nEp-3R0i6t`DQ{$a`2X=UFN$AQyYZ@$$i9zNWvcgPon9XOTGp$L&ocSf zscWIdtY^-=T^kj7YRCI0(vLzfKH9Y4#@X}7-LgLI-TdKTyWv6!4YTIY*Ltr2Eu;{t7dRm$(R z*sXXsBly$(Wvn;-{w!R1WWkKj`Iqz?tm?TH&++(nOHXrUd@#$pJox>#iK|}~-kqn| z-cnaC5IsSWf3?@d2FIUq;d@;-_9<*#wE4KG&hs^$D{nmU{cEtN?!=63nTK}2HTWSj z(aL`JrB?I%XIGw9UL37m9i;kFGsX1&*PEOt?>6*%T%5ub%YEUu33qCP{qlLS6Zgxr z`I>+0zaE!3Ey{Op0@Q2FuAyq+^xwBmFu{g%GvEC0gxSFl;4{L{gy z+}lDlu2eaw@EyxpE^hes@HMvL=*nDCIi^_|0U_^K%ug3s#Id5>rTR3Zq7?n&8&T`40WT3SDw5kYG{MLsD z177h8EM+~iX^oDt*F=WO&5l)<9$SXCUwrYd-Y`2s$*Wh6W%=XQiD@dmBDcP!6wC=b zzow>9f}wI!rNPO~PP{HZg#)&@b_v#X2JvrRuy*E+2bPtGg4uOzZfRcCyY0W^=#KfRo%LVuzAXF9cx1+!(-Kd(xevtpc6NDx ztoU-!bjbsm3t!i5pXiwsRCF}ZA~ETFg+0?6Z5PwTxupV^1eCHRUp+jusmZsURrqZ) z*8<~HUz;Z6H*TNSm60c!lm9*2U2l7Flg{=VKTNB1nS*aL@6d|Lt_*lm^mqTJI)){S z3*su(CVS4DYM^2F;Ev$L174xCN>~4U)HmJZYqXMDVE9Ht#J`XtY_N};oSO51b>eo6lq7Pkc~L*m{pQ9N93C>!~4 zL6S?_Nd;xJ?V z-u|^W+k*=BN?C_g_X@UrxqjPlR`tpMhN^djt64>^ySVay_|$Oj=e7=`ls{^hztrto z`)Cqxz^urdO@H$qvn&n1;~4sB;gV~Si`KS^OqpMsvFrR*6_-P9<{^Go@-LMpx9eM7 z+LEtowZAQAVc9{&=ap0UhpwHzM%iddkgVw0%yieRl9{D#{r5U8bzd(3AAKWiT9T-I zdA-PMZzhhW_r2OYkJ;Yxl$f$h_8(Gy`G{?oL}6WoeP^}8N%=H;8~5gQ?<&5yyl8!Y zH{lF##iD>Uflo9|{&g;hzQlFMtesa}Oa5z~jac>LqZ=og_AFjhnX@Y6_J;nKEeqP- zIU2^EezA-t{l;varHP;aZThy;dFs+>!4B2C&$eYW31=JLI`Hyp>C*#^oXP2ij*~T* zKVN!f_Q~a@gVnk7_M0y9ToMgkW2p4^d&0L*mklzP^c}dKaVWxi?zEln+*bG&ch;-R zxxC>>eRo4qH)uy2f51iMcLuw!C<^hl>&HJmR;g&KwO=fwPW;%5DP1fpo%j7LNL{kV zk>A`_N!S17pQK)u4ME%+#8NV2{N}UWHhXNj>FLYuGOH6EO1<5?SX#7?`o2|qyLg6s zh?vc)3>E3CQ*ZV~t-Z_lbEDA3jX9jxuBZ#;X{t|eVBwq`^0oH)hx~`9-8_%pi;9*C zSbNTwYr&P=e`YdH{Kwj_^KVwSEa82ta#hE)ew$3fykELmE4>S5CiN}r>}boERZV-x R9xW_fc6jy~3vGiF3jlg~0P_F< diff --git a/.travis/mvn-settings.xml b/.travis/mvn-settings.xml deleted file mode 100644 index 5480a96..0000000 --- a/.travis/mvn-settings.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - ossrh - ${env.OSSRH_USERNAME} - ${env.OSSRH_PASSWORD} - - - - - - ossrh - - true - - - gpg - ${env.GPG_KEY_NAME} - ${env.GPG_PASSPHRASE} - - - - \ No newline at end of file diff --git a/FAQ.md b/FAQ.md deleted file mode 100644 index d4b1292..0000000 --- a/FAQ.md +++ /dev/null @@ -1,53 +0,0 @@ -EvalEx - Frequently Asked Questions (and answers) -========== -Please feel free to add content to this list. - -### Can I use hexadecimal values? - -Yes e.g., `BigDecimal result = new Expression("0xcafe + 0xbabe").eval();` will be correctly -evaluated by EvalEx. - -### Incorrect results with large numbers - -The default math context used in expressions has a precision of 7, which is too small for large -numbers. Either create the expression with a different math context, or specify the precision before -evaluating: - -````Java - Expression expression=new Expression("timestamp >= 1500551315569",new MathContext(13)); -```` - -or - -````Java - expression.with("timestamp","1500551315568").setPrecision(13).eval(); -```` - -### Results are in scientific notation (e.g. 4E+1) - -When evaluation an expression, e.g.: - -````Java - String result=new Expression("5*8").eval(); -```` - -The string value in result will read `4E+1`. The reason is, that Expression.eval() returns -a `java.math.BigDecimal` -and the `toString()` method of `BigDecimal` uses scientific notation. -Use `Expression.eval().toPlainString()` instead. - -### -2^2 returns 4 instead of -4 - -There is a bit confusion among different calculators on how the precedence and association is -handled in combination with the power of operator. Excel, for example, evaluates above expression to -4, but Google calculator -4. The default precedence for the power of operator in EvalEx is lower -than the unary minus operator and therefore, -2 is calculated first, which results in `(-2)*(-2)`, -which calculates to 4. The precedence of the power operator can be adjusted when an expression is -created: - -````Java - ExpressionSettings settings=ExpressionSettings.builder() - .powerOperatorPrecedenceHigher() - .build(); - BigDecimal result=new Expression("-2^2",settings).eval(); -```` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index d823901..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,20 +0,0 @@ -Copyright 2012-2021 Udo Klimaschewski - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 8dc8a3d..38eba5a 100644 --- a/README.md +++ b/README.md @@ -1,405 +1,141 @@ EvalEx - Java Expression Evaluator ========== -[![Build Status](https://travis-ci.com/uklimaschewski/EvalEx.svg?branch=master)](https://travis-ci.com/uklimaschewski/EvalEx) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=uklimaschewski_EvalEx&metric=alert_status)](https://sonarcloud.io/dashboard?id=uklimaschewski_EvalEx) -| :warning: Starting with version 3, [EvalEx will have a new repository location](https://github.com/ezylang/evalex-core)! See also the [announcement in the discussion area](https://github.com/uklimaschewski/EvalEx/discussions/291)| -| --- | - -### Introduction - -EvalEx is a handy expression evaluator for Java, that allows to evaluate simple mathematical and -boolean expressions. - -Key Features: - -- Uses BigDecimal for calculation and result -- No dependencies to external libraries -- Precision and rounding mode can be set -- Supports variables -- Standard boolean and mathematical operators -- Standard basic mathematical and boolean functions -- Custom functions and operators can be added at runtime -- Functions can be defined with a variable number of arguments (see MIN and MAX functions) -- Supports for hexadecimal numbers and scientific notations of numbers -- Supports string literals in functions +![example workflow](https://github.com/ezylang/EvalEx/actions/workflows/maven.yml/badge.svg) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=EvalEx&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=EvalEx) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=EvalEx&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=EvalEx) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=EvalEx&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=EvalEx) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=EvalEx&metric=coverage)](https://sonarcloud.io/summary/new_code?id=EvalEx) + +EvalEx is a handy expression evaluator for Java, that allows to parse and evaluate expression +strings. + +## Key Features: + +- Supports numerical, boolean, string, array and structure expressions, operations and variables. +- Array and structure support: Arrays and structures can be mixed, building arbitrary data + structures. +- Uses BigDecimal for numerical calculations. +- MathContext and number of decimal places can be configured, with optional automatic rounding. +- No dependencies to external libraries. +- Easy integration into existing systems to access data. +- Predefined boolean and mathematical operators. +- Predefined mathematical, boolean and string functions. +- Custom functions and operators can be added. +- Functions can be defined with a variable number of arguments (see MIN, MAX and SUM functions). +- Supports hexadecimal and scientific notations of numbers. - Supports implicit multiplication, e.g. (a+b)(a-b) or 2(x-y) which equals to (a+b)\*(a-b) or 2\*( x-y) +- Lazy evaluation of function parameters (see the IF function) and support of sub-expressions. -### Discussion - -For announcements, questions and ideas visit the [Discussions area](https://github.com/uklimaschewski/EvalEx/discussions). - -### Download / Maven +## Documentation -You can download the binaries, source code and JavaDoc jars -from [Maven Central](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22EvalEx%22%20g%3A%22com.udojava%22) -. +The full documentation for EvalEx can be found +on [GitHub Pages](https://ezylang.github.io/EvalEx/) -To include it in your Maven project, refer to the artifact in your pom. For example: +## Discussion -````xml +For announcements, questions and ideas visit +the [Discussions area](https://github.com/ezylang/EvalEx/discussions). - - - com.udojava - EvalEx - - 2.7 - - -```` +## Examples -If you're using gradle add to your project's app build.gradle: +### A simple example, that shows how it works in general: -````gradle -dependencies { - ... - compile 'com.udojava:EvalEx:2.7' -} -```` +```java +Expression expression = new Expression("1 + 2 / (4 * SQRT(4))"); -### FAQ +EvaluationValue result = expression.evaluate(); -A list of frequently asked questions (and answers) can be found -here: [FAQ](https://github.com/uklimaschewski/EvalEx/blob/master/FAQ.md) +System.out.println(result.getNumberValue()); // prints 1.25 +``` -### Usage Examples +### Variables can be specified in the expression and their values can be passed for evaluation: -````java - BigDecimal result=null; +```java +Expression expression = new Expression("(a + b) * (a - b)"); - // Simple usage with an expression without variables. - Expression expression=new Expression("1+1/3"); - result=expression.eval(); // 1.333333 - // Lowering the precision. - expression.setPrecision(2); - result=expression.eval(); // 1.3 +EvaluationValue result = expression + .with("a", 3.5) + .and("b", 2.5) + .evaluate(); - // A more complex expression showing support for unary operators. - result=new Expression("(3.4 + -4.1)/2").eval(); // -0.35 +System.out.println(result.getNumberValue()); // prints 6.00 +``` - // Using functions and variables. - result=new Expression("SQRT(a^2 + b^2)") - .with("a","2.4") - .and("b","9.253") - .eval(); // 9.5591845 +### Boolean expressions produce a boolean result: - // Using pre-created BigDecimals for variables - BigDecimal a=new BigDecimal("2.4"); - BigDecimal b=new BigDecimal("9.235"); - result=new Expression("SQRT(a^2 + b^2)") - .with("a",a) - .and("b",b) - .eval(); // 9.5591845 +```java +Expression expression = new Expression("level > 2 || level <= 0"); - // Increasing the precision and setting a different rounding mode. - result=new Expression("2.4/PI") - .setPrecision(128) - .setRoundingMode(RoundingMode.UP) - .eval(); // 0.763943726841... +EvaluationValue result = expression + .with("level", 3.5) + .evaluate(); - // Using a function to receive a random number and test it. - result=new Expression("random() > 0.5").eval(); // 1 +System.out.println(result.getBooleanValue()); // prints true +``` - // Using more functions and showing the boolean support. - result=new Expression("not(x<7 || sqrt(max(x,9,3,min(4,3))) <= 3)") - .with("x","22.9") - .eval(); // 1 +### Like in Java, strings and text can be mixed: - // Calling a pre-defined function. - result=new Expression("log10(100)").eval(); // 2 -```` +```java +Expression expression = new Expression("\"Hello \" + name + \", you are \" + age") + .with("name","Frank") + .and("age",38); -### Precision +System.out.println(expression.evaluate().getStringValue()); // prints Hello Frank, you are 38 +``` -The default precision is set to 7 digits (`MathContext.DECIMAL32`). Depending on your use-case you -will want to set a different precision to get accurate results: +### Arrays are supported and can be passed as Java _Lists_. +See the [Documentation](https://ezylang.github.io/pages-playground/concepts/datatypes.html#array) +for more details. ```java - new Expression("1/3") - .setPrecision(3) - .eval(); // 0.333 +Expression expression = new Expression("values[i-1] * factors[i-1]"); - new Expression("1/3") - .setPrecision(12) - .eval(); // 0.333333333333 +EvaluationValue result = expression + .with("values", List.of(2, 3, 4)) + .and("factors", List.of(2, 4, 6)) + .and("i", 1) + .evaluate(); + +System.out.println(result.getNumberValue()); // prints 4 ``` -If you do not increase the precision as needed, you will get inaccurate results: +### Structures are supported and can be passed as Java _Maps_. +Arrays and Structures can be combined to build arbitrary data structures. See +the [Documentation](https://ezylang.github.io/pages-playground/concepts/datatypes.html#structure) +for more details. ```java - new Expression("123456789 + 123456789").eval(); // 246913600 +Map order = new HashMap<>(); +order.put("id", 12345); +order.put("name", "Mary"); + +Map position = new HashMap<>(); +position.put("article", 3114); +position.put("amount", 3); +position.put("price", new BigDecimal("14.95")); + +order.put("positions", List.of(position)); - new Expression("123456789 + 123456789") - .setPrecision(12) - .eval(); // 246913578 +Expression expression = new Expression("order.positions[x].amount * order.positions[x].price") + .with("order", order) + .and("x", 0); + +BigDecimal result = expression.evaluate().getNumberValue(); + +System.out.println(result); // prints 44.85 ``` -### Default Settings - -The default settings for an expression can be set on creation through an `ExpressionSettings` -object. It can be created using a builder pattern: - -````java -ExpressionSettings settings=ExpressionSettings.builder() - .mathContext(MathContext.DECIMAL128) - .powerOperatorPrecedenceHigher() - .build(); - new Expression("-2^2",settings).eval(); -```` - -### Supported Operators - - - - - - - - - - -
Mathematical Operators
OperatorDescription
+Additive operator / Unary plus
-Subtraction operator / Unary minus
*Multiplication operator, can be omitted in front of an open bracket
/Division operator
%Remainder operator (Modulo)
^Power operator
- - - - - - - - - - - - - -
Boolean Operators*
OperatorDescription
=Equals
==Equals
!=Not equals
<>Not equals
<Less than
<=Less than or equal to
>Greater than
>=Greater than or equal to
&&Boolean and
||Boolean or
-*Boolean operators always result in a BigDecimal value of 1 or 0 (zero). Any non-zero value is treated as a _true_ value. Boolean _not_ is implemented by a function. - -### Supported Functions - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Function*Description
NOT(expression)Boolean negation, 1 (means true) if the expression is not zero
IF(condition,value_if_true,value_if_false)Returns one value if the condition evaluates to true or the other if it evaluates to false
RANDOM()Produces a random number between 0 and 1
MIN(e1,e2, ...)Returns the smallest of the given expressions
MAX(e1,e2, ...)Returns the biggest of the given expressions
ABS(expression)Returns the absolute (non-negative) value of the expression
ROUND(expression,precision)Rounds a value to a certain number of digits, uses the current rounding mode
FLOOR(expression)Rounds the value down to the nearest integer
CEILING(expression)Rounds the value up to the nearest integer
LOG(expression)Returns the natural logarithm (base e) of an expression
LOG10(expression)Returns the common logarithm (base 10) of an expression
SQRT(expression)Returns the square root of an expression
SINR(expression)Returns the trigonometric sine of an angle (in radians)
COSR(expression)Returns the trigonometric cosine of an angle (in radians)
TANR(expression)Returns the trigonometric tangensuiju of an angle (in radians)
COTR(expression)Returns the trigonometric cotangens of an angle (in radians)
SECR(expression)Returns the secant (in radians)
CSCR(expression)Returns the cosecant (in radians)
ASINR(expression)Returns the angle of asin (in radians)
ACOSR(expression)Returns the angle of acos (in radians)
ATANR(expression)Returns the angle of atan (in radians)
ACOTR(expression)Returns the angle of acot (in radians)
ATAN2R(y,x)Returns the angle of atan2 (in radians)
SIN(expression)Returns the trigonometric sine of an angle (in degrees)
COS(expression)Returns the trigonometric cosine of an angle (in degrees)
TAN(expression)Returns the trigonometric tangens of an angle (in degrees)
COT(expression)Returns the trigonometric cotangens of an angle (in degrees)
SEC(expression)Returns the secant (in degrees)
CSC(expression)Returns the cosecant (in degrees)
ASIN(expression)Returns the angle of asin (in degrees)
ACOS(expression)Returns the angle of acos (in degrees)
ATAN(expression)Returns the angle of atan (in degrees)
ACOT(expression)Returns the angle of acot (in degrees)
ATAN2(y,x)Returns the angle of atan2 (in degrees)
SINH(expression)Returns the hyperbolic sine of a value
COSH(expression)Returns the hyperbolic cosine of a value
TANH(expression)Returns the hyperbolic tangens of a value
COTH(expression)Returns the hyperbolic cotangens of a value
SECH(expression)Returns the hyperbolic secant (in degrees)
CSCH(expression)Returns the hyperbolic cosecant (in degrees)
ASINH(expression)Returns the angle of hyperbolic sine (in degrees)
ACOSH(expression)Returns the angle of hyperbolic cosine (in degrees)
ATANH(expression)Returns the angle of hyperbolic tangens of a value
RAD(expression)Converts an angle measured in degrees to an approximately equivalent angle measured in radians
DEG(expression)Converts an angle measured in radians to an approximately equivalent angle measured in degrees
FACT(expression)Retuns the factorial value of an integer. Will return 1 for 0 or a negative number
-*Functions names are case insensitive. - -### Supported Constants - - - - - - - -   -
ConstantDescription
eThe value of e, exact to 70 digits
PIThe value of PI, exact to 100 digits
TRUEThe value one
FALSEThe value zero
NULLThe null value
- -### Add Custom Operators - -Custom operators can be added easily, simply create an instance of `Expression.Operator` and add it -to the expression. Parameters are the operator string, its precedence and if it is left associative. -The operators `eval()` method will be called with the BigDecimal values of the operands. All -existing operators can also be overridden. - -For example, add an operator `x >> n`, that moves the decimal point of _x_ _n_ digits to the right: - -````java -Expression e=new Expression("2.1234 >> 2"); - - e.addOperator(new AbstractOperator(">>",30,true){ -@Override -public BigDecimal eval(BigDecimal v1,BigDecimal v2){ - return v1.movePointRight(v2.toBigInteger().intValue()); - } - }); - - e.eval(); // returns 212.34 -```` - -Or another example, add a postfix unary operator `n!`, that calculates the factorial of n. The -parameters for postfix unary operators are the operator's string, its precedence, if it is left -associative, is it is boolean and if it is unary (true). - -````java -Expression e=new Expression("4!"); - - e.addOperator(new AbstractOperator("!",Expression.OPERATOR_PRECEDENCE_POWER_HIGHER+1,true,false,true){ -@Override -public BigDecimal eval(BigDecimal v1,BigDecimal v2){ - if(v1==null){ - throw new ArithmeticException("Operand may not be null"); - } - if(v1.remainder(BigDecimal.ONE)!=BigDecimal.ZERO){ - throw new ArithmeticException("Operand must be an integer"); - } - BigDecimal factorial=v1; - v1=v1.subtract(BigDecimal.ONE); - if(factorial.compareTo(BigDecimal.ZERO)==0||factorial.compareTo(BigDecimal.ONE)==0){ - return BigDecimal.ONE; - }else{ - while(v1.compareTo(BigDecimal.ONE)>0){ - factorial=factorial.multiply(v1); - v1=v1.subtract(BigDecimal.ONE); - } - return factorial; - } - } - }); - - e.eval(); // returns 24 -```` - -### Add Custom Functions - -Adding custom functions is as easy as adding custom operators. Create an instance -of `Expression.Function`and add it to the expression. Parameters are the function name and the count -of required parameters. The functions `eval()` method will be called with a list of the BigDecimal -parameters. A `-1` as the number of parameters denotes a variable number of arguments. All existing -functions can also be overridden. - -For example, add a function `average(a,b,c)`, that will calculate the average value of a, b and c: - -````java -Expression e=new Expression("2 * average(12,4,8)"); - - e.addFunction(new AbstractFunction("average",-1){ -@Override -public BigDecimal eval(List parameters){ - if(parameters.size()==0){ - throw new ExpressionException("average requires at least one parameter"); - } - BigDecimal avg=new BigDecimal(0); - for(BigDecimal parameter:parameters){ - avg=avg.add(parameter); - } - return avg.divide(new BigDecimal(parameters.size())); - } - }); - - e.eval(); // returns 16 -```` - -#### Custom Functions With String Parameters - -You can create a custom function with string parameters. Create an instance -of `Expression.LazyFunction`and add it to the expression. Parameters are the function name and the -count of required parameters. The functions `lazyEval()` method will be called with a list of the -LazyNumber parameters. A `-1` as the number of parameters denotes a variable number of arguments. -String parameters needs to be surrounded by `"`. - -For example, add a function `STREQ("string1","string2")`, that will compare whether string1 and -string2 are equal: - -````java -Expression e=new Expression("STREQ(\"test\", \"test2\")"); - e.addLazyFunction(new AbstractLazyFunction("STREQ",2){ -private LazyNumber ZERO=new LazyNumber(){ -public BigDecimal eval(){ - return BigDecimal.ZERO; - } -public String getString(){ - return"0"; - } - }; -private LazyNumber ONE=new LazyNumber(){ -public BigDecimal eval(){ - return BigDecimal.ONE; - } -public String getString(){ - return null; - } - }; -@Override -public LazyNumber lazyEval(List lazyParams){ - if(lazyParams.get(0).getString().equals(lazyParams.get(1).getString())){ - return ONE; - } - return ZERO; - } - }); - - e.eval(); // returns 0 -```` - -### How to contribute - -#### How to make a clean pull request - -- Create a personal fork of EvalEx on GitHub. -- Clone the fork on your local machine. Your remote repo on GitHub is called origin. -- Add the original repository as a remote called upstream. -- If you created your fork a while ago be sure to pull upstream changes into your local repository. -- Create a new branch to work on. Branch from master. -- Implement/fix your feature, comment your code. -- Follow the code style of EvalEx (Google code style), including indentation. -- If the project has tests run them! -- Add unit tests that test your new code. -- In general, avoid changing existing tests, as they also make sure the existing public API is - unchanged. -- Add or change the documentation as needed. -- Squash your commits into a single commit with git's interactive rebase. -- Push your branch to your fork on GitHub, the remote origin. -- From your fork open a pull request in the correct branch. Target the EvalEx's master branch. -- Once the pull request is approved and merged you can pull the changes from upstream to your local - repo and delete your branch. -- Last but not least: Always write your commit messages in the present tense. Your commit message - should describe what the commit, when applied, does to the code – not what you did to the code. - -### Author and License - -Copyright 2012-2021 by Udo Klimaschewski +## Author and License + +Copyright 2012-2022 by Udo Klimaschewski **Thanks to all who contributed to this -project: [Contributors](https://github.com/uklimaschewski/EvalEx/graphs/contributors)** +project: [Contributors](https://github.com/ezylang/EvalEx/graphs/contributors)** -The software is licensed under the MIT Open Source license ( -see [LICENSE](https://raw.githubusercontent.com/uklimaschewski/EvalEx/master/LICENSE) file). +The software is licensed under the Apache License, Version 2.0 ( +see [LICENSE](https://raw.githubusercontent.com/ezylang/EvalEx/main/LICENSE) file). * The *power of* operator (^) implementation was copied from [Stack Overflow](http://stackoverflow.com/questions/3579779/how-to-do-a-fractional-power-on-bigdecimal-in-java) @@ -407,9 +143,3 @@ see [LICENSE](https://raw.githubusercontent.com/uklimaschewski/EvalEx/master/LIC * The SQRT() function implementation was taken from the book [The Java Programmers Guide To numerical Computing](http://www.amazon.de/Java-Number-Cruncher-Programmers-Numerical/dp/0130460419) ( Ronald Mak, 2002) -* Varargs implementation based on "David's method" outlined in Gene Pavlovsky's comment - from [here](http://www.kallisti.net.nz/blog/2008/02/extension-to-the-shunting-yard-algorithm-to-allow-variable-numbers-of-arguments-to-functions/#comment-125789) - -### Other projects based on EvalEx - -* [A port of EvalEx to Dart](https://github.com/RobluScouting/EvalEx) diff --git a/deploy.sh b/deploy.sh deleted file mode 100644 index e3864dc..0000000 --- a/deploy.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash -# expects variables to be set: -# - OSSRH_USERNAME -# - OSSRH_PASSWORD -# - GPG_KEY_NAME -# - GPG_PASSPHRASE -# expects file to exist: -# - .travis/gpg.asc - -set -e - -echo "starting deploy script deploy.sh ..." - -# Check the variables are set -if [ -z "$OSSRH_USERNAME" ]; then - echo "missing environment value: OSSRH_USERNAME" >&2 - exit 1 -fi - -if [ -z "$OSSRH_PASSWORD" ]; then - echo "missing environment value: OSSRH_PASSWORD" >&2 - exit 1 -fi - -if [ -z "$GPG_KEY_NAME" ]; then - echo "missing environment value: GPG_KEY_NAME" >&2 - exit 1 -fi - -if [ -z "$GPG_PASSPHRASE" ]; then - echo "missing environment value: GPG_PASSPHRASE" >&2 - exit 1 -fi - -# Prepare the local keyring (requires travis to have decrypted the file -# beforehand) -echo "importing gpg keys" -gpg -v --fast-import .travis/gpg.asc - -if [ -n "$TRAVIS_TAG" ] -then - echo "on a tag -> set pom.xml to $TRAVIS_TAG" - mvn --settings "${TRAVIS_BUILD_DIR}/.travis/mvn-settings.xml" org.codehaus.mojo:versions-maven-plugin:2.1:set -DnewVersion="$TRAVIS_TAG" 1>/dev/null 2>/dev/null -else - echo "not on a tag -> keep snapshot version in pom.xml" >&2 -fi - -# Run the maven deploy steps -mvn deploy -P publish -DskipTests=true --settings "${TRAVIS_BUILD_DIR}/.travis/mvn-settings.xml" diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..10f9906 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,18 @@ +# EvalEx Documentation + +For documentation, EvalEx uses GitHub Pages, which uses Jekyll to transform markdown to static HTML. + +The site HTML is generated whenever something is committed to the main branch. + +Jekyll will then transform all files it finds in the "docs" directory. + +For the layout the JustTheDocs template is used, which supports a lot of good things like a sidebar +menu, page hierarchy and searching. + +## EvalEx documentation home: + +https://ezylang.github.io/EvalEx/ + +## Using JustTheDocs template: + +https://just-the-docs.github.io/just-the-docs/ diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..84c7b50 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,6 @@ +remote_theme: just-the-docs/just-the-docs +title: EvalEx Documentation +aux_links: + "View on GitHub": + - "//github.com/ezylang/EvalEx" +footer_content: "Copyright © 2012-2022 Udo Klimaschewski" diff --git a/docs/concepts/concepts.md b/docs/concepts/concepts.md new file mode 100644 index 0000000..106ed00 --- /dev/null +++ b/docs/concepts/concepts.md @@ -0,0 +1,6 @@ +--- +layout: default +title: Concepts +nav_order: 2 +has_children: true +--- diff --git a/docs/concepts/datatypes.md b/docs/concepts/datatypes.md new file mode 100644 index 0000000..c45615e --- /dev/null +++ b/docs/concepts/datatypes.md @@ -0,0 +1,167 @@ +--- +layout: default +title: Data types +parent: Concepts +nav_order: 2 +--- + +## Data Types + +EvalEx supports the following data types: + +| Data Type | Internal Representation | +|-----------------|-----------------------------------| +| NUMBER | java.math.BigDecimal | +| BOOLEAN | java.lang.Boolean | +| STRING | java.lang.String | +| ARRAY | java.util.List | +| STRUCTURE | java.util.Map | +| EXPRESSION_NODE | com.ezylang.evalex.parser.ASTNode | + +Data is stored in an _EvaluationValue_, which holds the value and the data type. + +### NUMBER + +Numbers are stored as _java.math.BigDecimal_ values and are either passed directly, or will +automatically be converted from one of the following Java data types: + +- long +- int +- short +- byte +- double +- float + +Be careful when passing double or float values, as these have to be converted from their +floating-point arithmetic representation to a BigDecimal representation, which stores values a +signed decimal number of arbitrary precision with an associated scale. + +### BOOLEAN + +Booleans can be passed directly as variable values to an expression evaluation, or are the result of +a boolean operation or function. + +If _STRING_ or _NUMBER_ values are used in boolean expressions, they are converted using the +following rules: + +- If the value is a _NUMBER_, it is false if it equals to zero. All other values evaluate to true. +- If the value is a _STRING_, it is true if it equals to "true" (any case), false otherwise. + +Consider this example, it will evaluate to true: + +```java +Expression expression = new Expression("stringValue && numberValue") + .with("stringValue", "True") + .and("numberValue", 42); +``` + +### STRING + +Any instance of _java.lang.CharSequence_ or _java.lang.Character_ will automatically be converted to +a _STRING_ datatype. Conversion will be done by invoking the _toString()_ method on the input +object. + +### ARRAY + +Arrays are stored internally as a _java.util.List<EvaluationValue>_. When passed as a +variable, the list will be iterated and each entry will be converted using the data type conversion +rules. +So, for example, a list of double values will be converted to a list of _EvaluationValue_ +objects of type _NUMBER_, with an internal BigDecimal representation. + +Arrays can hold mixed data types: + +```java +List list = List.of(2.5, "Hello", true); + +// prints "true" +System.out.println( + new Expression("list[2]").with("list", list).evaluate().getStringValue()); +``` + +Array index starts at 0 with the first array element. + +Arrays can hold other arrays as members, which can hold other arrays and so on: + +```java +List list1 = List.of(1, 2, 3); +List list2 = List.of(4, 5, 6); +List sublist = List.of(100, 200, 300); +List list3 = List.of(7, 8, sublist); + +List list = List.of(list1, list2, list3); + +// prints 200 +System.out.println( + new Expression("list[2][2][1]").with("list", list).evaluate().getStringValue()); +``` + +Arrays can also hold _STRUCTURE_ elements as entries. + +### STRUCTURE + +Structures are stored internally as a _java.util.Map<String, EvaluationValue>_. When passed as +a variable, the map entries will be iterated and each entry value will be converted using the data +type conversion rules. + +Structures can hold other structures, which can form a tree like data structure. + +Arrays and structures can be combined in any arbitrary way. + +```java +Map order = new HashMap<>(); +order.put("id", 12345); +order.put("name", "Mary"); + +Map position = new HashMap<>(); +position.put("article", 3114); +position.put("amount", 3); +position.put("price", new BigDecimal("14.95")); + +order.put("positions", List.of(position)); + +Expression expression = new Expression("order.positions[x].amount * order.positions[x].price") + .with("order", order) + .and("x", 0); + +BigDecimal result = expression.evaluate().getNumberValue(); + +System.out.println(result); // prints 44.85 +``` + +### EXPRESSION_NODE + +A string expression is converted into an abstract syntax tree (AST), which represents the expression +structure and evaluation order. When an expression is evaluated, the evaluation starts at the lowest +tree (leaf) node and works its way up to the root node, which then gives the expression result +value. + +The internal object for such a node is the _com.ezylang.evalex.parser.ASTNode_. It contains the +token associated to the node (e.g. a multiplication operator, or a function name) and has zero or +more children (the operator or function parameters). + +By passing an _ASTNode_ and its substructure as a variable, this will be handled as a subtree and +will be evaluated when the node is evaluated. + +With this, a _lazy evaluation_ is possible, only evaluating a node when it is desired (see the +_IF()_ function for an example. Also, it allows the creation of sub-expressions. + +The _Expression_ object has a method to create such sub-expressions, returning the root node of an +arbitrary string expression: + +```java +Expression expression = new Expression("a*b"); + +ASTNode subExpression = expression.createExpressionNode("4+3"); + +EvaluationValue result = expression + .with("a", 2) + .and("b", subExpression) + .evaluate(); + +System.out.println(result); // prints 14 +``` + +Note that the above expression is not evaluated as "2 * 4 + 3", which would result in 11. +Instead, the sub-expression "4 + 3" is calculated first, when it comes to finding the value of the +variable _b_. Resulting in calculation of "2 * 7", which is 14. diff --git a/docs/concepts/parsing_evaluation.md b/docs/concepts/parsing_evaluation.md new file mode 100644 index 0000000..2f32c0c --- /dev/null +++ b/docs/concepts/parsing_evaluation.md @@ -0,0 +1,134 @@ +--- +layout: default +title: Parsing and Evaluation +parent: Concepts +nav_order: 1 +--- + +## Parsing and Evaluation + +### Parsing + +To parse and evaluate an expression, you first create an expression object: + +```java +Expression expression = new Expression("1 + 2 / (4 * SQRT(4))"); +``` + +Optionally, you can create a custom configuration and pass this in the constructor: + +```java +ExpressionConfiguration configuration = ExpressionConfiguration.builder() + .decimalPlacesRounding(2) + .build(); + +Expression expression = new Expression("1 + 2 / (4 * SQRT(4))", configuration); +``` + +Creating the expression object does not parse or validate the expression. + +Parsing will be performed, when one of the following happens: + +- The expression is validated wih the _validate()_ method. +- The expression is evaluated with the _evaluate()_ method. +- An abstract syntax tree (AST) is created with the _getAbstractSyntaxTree()_ method or any public + method that uses it. + +Once an expression was parsed, the resulting abstract syntax tree is cached inside the expression +and wil be re-used on subsequent evaluations. + +### Evaluation + +Evaluation is done by traversing the parsed abstract syntax tree. If it has not been generated +before, it will be created and cached prior to the evaluation. + +```java +Expression expression = new Expression("1 + 2 / (4 * SQRT(4))"); + +EvaluationValue result = expression.evaluate(); + +System.out.println(result.getNumberValue()); // prints 1.25 +``` + +The evaluation will return an _EvaluationValue_ object. Depending on the expression, the +_EvaluationValue_ will be of one of the types: + +- NUMBER - If the expression resulted in a number. +- STRING - If the expression resulted in a string. +- BOOLEAN - If the expression resulted in a boolean value. +- ARRAY - If the expression resulted in an array. +- STRUCTURE - If the expression resulted in a structure. + +The _EvaluationValue_ has methods to check and retrieve/convert the evaluation value. + +See chapter [Data Types](datatypes.html) for details on the different data types. + +### Validation + +An expression can be validated with the _validate()_ method. If there is a parsing problem, the +method will throw a _ParseException_, which holds detailed information about the problem and its +location. + +### Passing Variables + +Variables can be used in an expression. Their value will be retrieved during evaluation by querying +the defined data accessor. + +by default, EvalEx uses the _MapBasedDataAccessor_, which stores the variable values in a +case-insensitive map. Each expression will have an own instance of the accessor, so that different +expression do not share the same data. + +Prior to evaluation, the variable values can be set by setting them one by one, using the _with()_ +and _and()_ methods. +(Both methods are the same, the name difference is just for convenience). + +```java +Expression expression = new Expression("(a + b) * (a - b)"); + +EvaluationValue result = expression + .with("a", 3.5) + .and("b", 2.5) + .evaluate(); + +System.out.println(result.getNumberValue()); // prints 6.00 +``` + +Alternatively, the variable values can be set by defining a map with names and values and then +passing it to +the _withValues()_ method: + +```java +Expression expression = new Expression("(a + b) * (a - b)"); + +Map values = new HashMap<>(); +values.put("a", 3.5); +values.put("b", 2.5); + +EvaluationValue result = expression.withValues(values).evaluate(); + +System.out.println(result.getNumberValue()); // prints 6.00 +``` + +The data conversion of the passed values will automatically be performed through the created +_EvaluationObject_. + +See chapter [Data Types](datatypes.html) for details on the conversion. + +Another option to have EvalEx use your data is to define a custom data accessor. + +See chapter [Data Access](../customization/data_access.html) for details. + +### Exception Handling + +In EvalEx, there are two general exceptions: + +- A _ParseException_ is thrown, when the expression could not be parsed. +- An _EvaluationException_ is thrown, when the expression could be parsed, but not evaluated. + +If possible, both exceptions report the following details: + +- Start Position of the error (character position, starting with 1). +- End Position of the error (character position, starting with 1). +- The Token string, usually the operator, function, variable or literal causing the error. +- The error message. + diff --git a/docs/concepts/rounding.md b/docs/concepts/rounding.md new file mode 100644 index 0000000..8f9ff8c --- /dev/null +++ b/docs/concepts/rounding.md @@ -0,0 +1,89 @@ +--- +layout: default +title: Precision, Scale and Rounding +parent: Concepts +nav_order: 3 +--- + +## Precision, Scale and Rounding + +EvalEx uses _java.math.BigDecimal_ values for (most) calculations and for internal storage of number +values and variables. + +In contrast to float or double values, which are stored in floating-point arithmetic, BigDecimal +values are stored as signed decimal number of arbitrary precision with an associated scale. + +This makes BigDecimal a perfect choice in applications, where the precise decimal values are needed, +e.g. in financial systems. + +### Precision + +In BigDecimal (and EvalEx), the precision is the number of digits in the unscaled value, independent +of the number of decimal digits. +Both of this values + +- 123.456 +- 1234.56 + +have a precision of 6, but have a different scale (the first has a scale of 3, the second of 2). + +Values that exceed the maximum precision, will be rounded according to the rounding mode. + +The default precision in EvalEx is 68. + +### Scale + +The scale of a BigDecimal is the number of decimal digits. Any BigDecimal can be rounded to a lower +number of scale (less decimal digits) using a rounding mode. + +### Rounding Mode + +EvalEx supports all rounding modes defined in _java.math.RoundingMode_: + +| Mode | Description | +|-------------|----------------------------------------------------------------------------------------------------------------------------------------------| +| CEILING | Rounding mode to round towards positive infinity. | +| DOWN | Rounding mode to round towards zero. | +| FLOOR | Rounding mode to round towards negative infinity. | +| HALF_DOWN | Rounding mode to round towards "nearest neighbor" unless both neighbors are equidistant, in which case round down. | +| HALF_EVEN | Rounding mode to round towards the "nearest neighbor" unless both neighbors are equidistant, in which case, round towards the even neighbor. | +| HALF_UP | Rounding mode to round towards "nearest neighbor" unless both neighbors are equidistant, in which case round up. | +| UNNECESSARY | Rounding mode to assert that the requested operation has an exact result, hence no rounding is necessary. | +| UP | Rounding mode to round away from zero. | + +The default rounding mode in EvalEx is _HALF_EVEN_. + +### Configuring Precision, Rounding Mode and Automatic Scaling + +Precision and rounding mode are configured through the _ExpressionConfiguration_ by specifying +the _java.math.MathContext_: + +```java +// set precision to 32 and rounding mode to HALF_UP +ExpressionConfiguration configuration = + ExpressionConfiguration.builder() + .mathContext(new MathContext(32, RoundingMode.HALF_UP)) + .build(); +``` + +Automatic scaling is disabled by default. When enabled, EvalEx will round all input variables, +intermediate operation and function results and the final result to the specified number of decimal +digits, using the current rounding mode: + +```java +// set precision to 32 and rounding mode to HALF_UP +ExpressionConfiguration configuration = + ExpressionConfiguration.builder() + .decimalPlacesRounding(2) + .build(); + +Expression expression = new Expression("2.128 + a", configuration); + +// prints 3.26 +System.out.println( + expression + .with("a", new BigDecimal("1.128")) + .evaluate() + .getNumberValue()); +``` + diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md new file mode 100644 index 0000000..02d5b62 --- /dev/null +++ b/docs/configuration/configuration.md @@ -0,0 +1,128 @@ +--- +layout: default +title: Configuration +nav_order: 3 +--- + +## Configuration + +EvalEx can be configured through an _ExpressionConfiguration_ object, which can be passed as a +parameter to the _Expression_ constructor. + +Example usage, showing all default configuration values: + +```java +ExpressionConfiguration configuration = ExpressionConfiguration.builder() + .arraysAllowed(true) + .dataAccessorSupplier(MapBasedDataAccessor::new) + .decimalPlacesRounding(ExpressionConfiguration.DECIMAL_PLACES_ROUNDING_UNLIMITED) + .defaultConstants(ExpressionConfiguration.StandardConstants) + .functionDictionary(ExpressionConfiguration.StandardFunctionsDictionary) + .implicitMultiplicationAllowed(true) + .mathContext(ExpressionConfiguration.DEFAULT_MATH_CONTEXT) + .operatorDictionary(ExpressionConfiguration.StandardOperatorsDictionary) + .powerOfPrecedence(OperatorIfc.OPERATOR_PRECEDENCE_POWER) + .structuresAllowed(true) + .build(); + +Expression expression = new Expression("2.128 + a", configuration); +``` + +### Arrays allowed + +Specifies if the array index function is allowed (default is true). If set to false, the expression +will throw a _ParseException_, if there is a '[' is encountered in the expression and also no +operator or function is defined for this character. + +### Data Accessor + +The Data Accessor is responsible for storing and retrieving variable values. + +See chapter [Data Access](../customization/data_access.html) for details. + +The default implementation is the _MapBasedDataAccessor_, which stores all variables in a +**case-insensitive** _Map_. + +The supplier is called whenever a new Expression is created, so that each expression can own an own +instance of an accessor. Custom implementations of the supplier and data accessor may allow +expressions to share the same space. + +Upon creation of an expression, the configured default constants will be added to the data accessor +through the set method. + +### Decimal Places Rounding + +Specifies the amount of decimal places to round to in each operation or function. +See chapter [Precision, Scale and Rounding](../concepts/rounding.html) for details. + +### Default Constants + +Specifies the default constants that can be used in every expression as a _Map_ with the constant +name and _EvaluationValue_ as value. +See the reference chapter for a +list: [Default Constants](../references/constants.html) + +### Function Dictionary + +The function dictionary is used to look up the functions that are used in an expression. + +See chapter [Function Dictionary](../customization/function_dictionary.html) for details. + +The default implementation is the _MapBasedFunctionDictionary_, which stores all variables in a +**case-insensitive** _Map_. + +### Implicit Multiplication + +Implicit multiplication automatically adds in expression like "(a+b)(b+c)" the missing +multiplication operator, so that the expression reads "(a+b) * (b+c)". + +By default, implicit multiplication is enabled. It can be disabled with this configuration +parameter. + +### Math Context + +The math context is used throughout all operations and functions. The default has a precision of 68 +and a rounding mode of _HALF_EVEN_. + +See chapter [Precision, Scale and Rounding](../concepts/rounding.html) for details. + +### Operator Dictionary + +The operator dictionary is used to look up the operators that are used in an expression. + +See chapter [Operator Dictionary](../customization/operator_dictionary.html) for details. + +The default implementation is the _MapBasedOperatorDictionary_, which stores all variables in a +**case-insensitive** _Map_. + +### Power Of Precedence + +In mathematics, there is no general rule which precedence the power-of operator has. +Consider the expression "-2^2". The expression can have two different results, depending on the +precedence: + +- If the precedence is lower than the unary minus, the result will be 4 (-2 * -2). +- If the precedence is higher than the unary minus, the result will be -4 -(2 * 2). + +By default, EvalEx uses a lower precedence. You can configure to use a higher precedence by +specifying it here, or by using a predefined constant: + +```java +ExpressionConfiguration configuration = ExpressionConfiguration.builder() + .powerOfPrecedence(OperatorIfc.OPERATOR_PRECEDENCE_POWER_HIGHER) + .build(); + +// will now result in -4, instead of 4: +Expression expression = new Expression("-2^2", configuration); +``` + +### Strip Trailing Zeros + +If set to true (default), then the trailing decimal zeros in a number result will be stripped. +E.g. a _BigDecimal_ result of _9.0_ will become _9_ and _-2.120000_ will become _-2.12_. + +### Structures Allowed + +Specifies if the structure separator ('.') operator is allowed (default is true). If set to false, +the expression will throw a _ParseException_, if the a '.' is encountered in the expression and also +no operator or function is defined for this character. diff --git a/docs/customization/custom_functions.md b/docs/customization/custom_functions.md new file mode 100644 index 0000000..7e4fd71 --- /dev/null +++ b/docs/customization/custom_functions.md @@ -0,0 +1,142 @@ +--- +layout: default +title: Custom Functions +parent: Customization +nav_order: 1 +--- + +## Custom Functions + +EvalEx comes with a predefined set of ready-to use functions. +A standard collection is added to the default configuration, using the +_ExpressionConfiguration.StandardFunctionsDictionary_. + +Function definition is made in two parts: + +- Defining the function parameters and logic by adding a class that implements the _FunctionIfc_ + interface. +- Adding the function with a unique name to the function dictionary, so it can be found and used in + expressions. + +### Defining the Function + +To ease some common implementation routines, a custom function usually extends the +_AbstractFunction_ class. +As an example, we can look at the _ROUND()_ function: + +```java +@FunctionParameter(name = "value") +@FunctionParameter(name = "scale") +public class RoundFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + EvaluationValue value = parameterValues[0]; + EvaluationValue precision = parameterValues[1]; + + return new EvaluationValue( + value + .getNumberValue() + .setScale( + precision.getNumberValue().intValue(), + expression.getConfiguration().getMathContext().getRoundingMode())); + } +} +``` + +As you can see, the only method that has to be implemented, is the _evaluate()_ method. It will be +called by EvalEx during evaluation. It will pass all parameters values to function. + +The expression itself and also the function token are passed to the call, they can be used to +access the configuration or to find out the function name and its position in the expression. + +#### Parameter Definition + +Parameters are defined by annotating the function class. Each _FunctionParameter_ has at least a +name. +In the above example, the function expects two parameters, the value and the scale. + +#### Variable Number of Arguments + +Some functions allow a variable number of arguments. For this, a parameter can have the flag _ +isVarArg_ set to _true_. + +> NOTE: Only the last parameter and only one parameter is allowed to have the _isVarArg_ flag set +> to _true_. + +For example, the _MAX()_ function makes use of a variable number of arguments: + +```java +@FunctionParameter(name = "value", isVarArg = true) +public class MaxFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + BigDecimal max = null; + for (EvaluationValue parameter : parameterValues) { + if (max == null || parameter.getNumberValue().compareTo(max) > 0) { + max = parameter.getNumberValue(); + } + } + return new EvaluationValue(max); + } +} +``` + +#### Lazy Parameter Evaluation + +Usually, a function parameter already holds the evaluated value. E.g. in "ROUND(4/3, 2)", the +function _evaluate()_ will be passed the already calculated result of "4/3". + +In some cases, this is not useful. Take for example the _IF()_ function. It has three parameters: + +- A boolean condition +- An expression value that is returned if the condition is _true_ +- An expression value that is returned if the condition is _false_ + +Here not both expressions should be evaluated, before the function is called. This may lead to +errors, e.g. in "IF(a == 0, 0, 2 / a)" the evaluation of "2 / a" would lead to a division by zero +error. + +Therefore, function parameters can be defined as "lazy", which means that they will not be +evaluated, before the function is called. Instead, the expression sub-nodes are passed as a +parameter and the function is responsible to evaluate the subexpression. + +This can be easily done, as the _IF()_ function demonstrates: + +```java +@FunctionParameter(name = "condition") +@FunctionParameter(name = "resultIfTrue", isLazy = true) +@FunctionParameter(name = "resultIfFalse", isLazy = true) +public class IfFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + if (parameterValues[0].getBooleanValue()) { + return expression.evaluateSubtree(parameterValues[1].getExpressionNode()); + } else { + return expression.evaluateSubtree(parameterValues[2].getExpressionNode()); + } + } +} +``` + +### Adding the Function + +You can always add the function directly to the function dictionary, using the +_addFunction(String functionName, FunctionIfc function)_ method. + +But there is an easier way by using the expression configuration, which also allows you to add more +than one function: + +```java +ExpressionConfiguration configuration = + ExpressionConfiguration.defaultConfiguration() + .withAdditionalFunctions( + Map.entry("MAX_VALUE", new MaxFunction()), + Map.entry("MIN_VALUE", new MinFunction())); + +Expression expression = new Expression("MAX_VALUE(1,2,3) + MIN_VALUE(7,8,9)"); +``` diff --git a/docs/customization/custom_operators.md b/docs/customization/custom_operators.md new file mode 100644 index 0000000..81958b2 --- /dev/null +++ b/docs/customization/custom_operators.md @@ -0,0 +1,86 @@ +--- +layout: default +title: Custom Operators +parent: Customization +nav_order: 2 +--- + +## Custom Operators + +EvalEx comes with a predefined set of ready-to use operators. +A standard collection is added to the default configuration, using the +_ExpressionConfiguration.StandardOperatorsDictionary_. + +Operation definition is made in two parts: + +- Defining the operator type and logic by adding a class that implements the _OperatorIfc_ + interface. +- Adding the operator with a unique name to the operator dictionary, so it can be found and used in + expressions. + +### Defining the Operator + +To ease some common implementation routines, a custom operator usually extends the +_AbstractOperator_ class. +As an example, we can look at the boolean "AND" operator: + +```java +@InfixOperator(precedence = OPERATOR_PRECEDENCE_AND) +public class InfixAndOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return new EvaluationValue(operands[0].getBooleanValue() && operands[1].getBooleanValue()); + } +} +``` + +As you can see, the only method that has to be implemented, is the _evaluate()_ method. It will be +called by EvalEx during evaluation. It will pass the operand values to the method. + +The expression itself and also the operator token are passed to the call, they can be used to +access the configuration or to find out the operator name and its position in the expression. + +#### Operator type + +The operator type is determined through the corresponding annotation: + +- @PrefixOperator +- @InfixOperator +- @PostfixOperator + +Infix operators will receive two operands in the _evaluate()_ method, pre- and postfix operator will +receive one operand. + +#### Operator Precedence and associativity + +The order of expression evaluation is determined by the operator precedence and its associativity. + +The precedence tells EvalEx which operator have to be evaluated first. With this, it is e.g. defined +that multiplication happens before addition. + +The associativity tells in which order (left to right, right to left) operators of the same +precedence are evaluated. + +Precedence and associativity can be specified with the operator annotation. + +There is a collection of predefined operator precedences in the _OperatorIfc_ interface. + +### Adding the Operator + +You can always add the operator directly to the operator dictionary, using the +_addOperator(String operatorString, OperatorIfc operator)_ method. + +But there is an easier way by using the expression configuration, which also allows you to add more +than one operator: + +```java +ExpressionConfiguration configuration = + ExpressionConfiguration.defaultConfiguration() + .withAdditionalOperators( + Map.entry("AND", new InfixAndOperator()), + Map.entry("OR", new InfixOrOperator())); + +Expression expression = new Expression("(a > 5 AND x < 10) OR (y < 0)"); +``` diff --git a/docs/customization/customization.md b/docs/customization/customization.md new file mode 100644 index 0000000..d13843a --- /dev/null +++ b/docs/customization/customization.md @@ -0,0 +1,6 @@ +--- +layout: default +title: Customization +nav_order: 4 +has_children: true +--- \ No newline at end of file diff --git a/docs/customization/data_access.md b/docs/customization/data_access.md new file mode 100644 index 0000000..63171e8 --- /dev/null +++ b/docs/customization/data_access.md @@ -0,0 +1,70 @@ +--- +layout: default +title: Data Access +parent: Customization +nav_order: 5 +--- + +## Data Access + +By default, EvalEx has its own storage for variable values, the _MapBasedDataAccessor_. +It stores and retrieves the variables in a case-insensitive _TreeMap_. +A separate instance of the _MapBasedDataAccessor_ will be created through the default configured +supplier for each new Expression, separating the storage for each expression instance. + +You can easily define your own data access interface, by defining a class that implements the +_DataAccessorInterface_: + +```java +public interface DataAccessorIfc { + + /** + * Retrieves a data value. + * + * @param variable The variable name, e.g. a variable or constant name. + * @return The data value, or null if not found. + */ + EvaluationValue getData(String variable); + + /** + * Sets a data value. + * + * @param variable The variable name, e.g. a variable or constant name. + * @param value The value to set. + */ + void setData(String variable, EvaluationValue value); +} +``` + +Only two methods need to be implemented: + +- getData(String variable) - should return a value for a variable name. +- setData(String variable, EvaluationValue value) - set a variable value. + +The _setData()_ method is only called when a variable is added to the expression, e.g. using the +_with()_ method. You may leave the implementation empty, if you do want to support this. + +> NOTE: The variable names that are passed to the methods are case-sensitive, just like they were +> entered in the expression. + +The custom data accessor can then be specified in the expression configuration: + +```java +ExpressionConfiguration configuration = ExpressionConfiguration.builder() + .dataAccessorSupplier(MyCustomDataAccessor::new) + .build(); + +Expression expression = new Expression("2.128 + a", configuration); +``` + +The example here creates a new instance of the _MyCustomDataAccessor_ for each new instance of an +expression. But you could also share an instance for all expressions: + +```java +final MyCustomDataAccessor customAccessor = new MyCustomDataAccessor(); +ExpressionConfiguration configuration = ExpressionConfiguration.builder() + .dataAccessorSupplier(() -> customAccessor) + .build(); + +Expression expression = new Expression("2.128 + a", configuration); +``` diff --git a/docs/customization/function_dictionary.md b/docs/customization/function_dictionary.md new file mode 100644 index 0000000..351921d --- /dev/null +++ b/docs/customization/function_dictionary.md @@ -0,0 +1,65 @@ +--- +layout: default +title: Function Dictionary +parent: Customization +nav_order: 3 +--- + +## Function Dictionary + +By default, EvalEx has its own dictionary for the functions that can be used in an expression, +the _MapBasedFunctionDictionary_. It stores and retrieves the functions in a case-insensitive +_TreeMap_. + +You can easily define your own function directory, by defining a class that implements the +_FunctionDictionaryIfc_: + +```java +public interface FunctionDictionaryIfc { + + /** + * Allows to add a function to the dictionary. Implementation is optional, if you have a fixed set + * of functions, this method can throw an exception. + * + * @param functionName The function name. + * @param function The function implementation. + */ + void addFunction(String functionName, FunctionIfc function); + + /** + * Check if the dictionary has a function with that name. + * + * @param functionName The function name to look for. + * @return true if a function was found or false if not. + */ + default boolean hasFunction(String functionName) { + return getFunction(functionName) != null; + } + + /** + * Get the function definition for a function name. + * + * @param functionName The name of the function. + * @return The function definition or null if no function was found. + */ + FunctionIfc getFunction(String functionName); +``` + +Only two methods need to be implemented: + +- addFunction(String functionName, FunctionIfc function) - which allows to add a function. +- FunctionIfc getFunction(String functionName) - retrieves a function implementation. + +The _addFunction()_ method is only called when a function is added to the configuration, using the +_withAdditionalFunctions()_ in the _ExpressionConfiguration_ object. + +> NOTE: The function names that are passed to the methods are case-sensitive, just like they were +> entered in the expression. + +The custom function dictionary can then be specified in the expression configuration: + +```java +ExpressionConfiguration configuration = ExpressionConfiguration.builder() + .functionDictionary(new MyFunctionDirectory()) + .build(); +``` diff --git a/docs/customization/operator_dictionary.md b/docs/customization/operator_dictionary.md new file mode 100644 index 0000000..dc6e62f --- /dev/null +++ b/docs/customization/operator_dictionary.md @@ -0,0 +1,104 @@ +--- +layout: default +title: Operator Dictionary +parent: Customization +nav_order: 4 +--- + +## Function Dictionary + +By default, EvalEx has its own dictionary for the operators that can be used in an expression, +the _MapBasedOperatorDictionary_. It stores and retrieves the operators in a case-insensitive +_TreeMap_. + +You can easily define your own operator directory, by defining a class that implements the +_FunctionDictionaryIfc_: + +```java +public interface OperatorDictionaryIfc { + + /** + * Allows to add an operator to the dictionary. Implementation is optional, if you have a fixed + * set of operators, this method can throw an exception. + * + * @param operatorString The operator name. + * @param operator The operator implementation. + */ + void addOperator(String operatorString, OperatorIfc operator); + + /** + * Check if the dictionary has a prefix operator with that name. + * + * @param operatorString The operator name to look for. + * @return true if an operator was found or false if not. + */ + default boolean hasPrefixOperator(String operatorString) { + return getPrefixOperator(operatorString) != null; + } + + /** + * Check if the dictionary has a postfix operator with that name. + * + * @param operatorString The operator name to look for. + * @return true if an operator was found or false if not. + */ + default boolean hasPostfixOperator(String operatorString) { + return getPostfixOperator(operatorString) != null; + } + + /** + * Check if the dictionary has an infix operator with that name. + * + * @param operatorString The operator name to look for. + * @return true if an operator was found or false if not. + */ + default boolean hasInfixOperator(String operatorString) { + return getInfixOperator(operatorString) != null; + } + + /** + * Get the operator definition for a prefix operator name. + * + * @param operatorString The name of the operator. + * @return The operator definition or null if no operator was found. + */ + OperatorIfc getPrefixOperator(String operatorString); + + /** + * Get the operator definition for a postfix operator name. + * + * @param operatorString The name of the operator. + * @return The operator definition or null if no operator was found. + */ + OperatorIfc getPostfixOperator(String operatorString); + + /** + * Get the operator definition for an infix operator name. + * + * @param operatorString The name of the operator. + * @return The operator definition or null if no operator was found. + */ + OperatorIfc getInfixOperator(String operatorString); +} +``` + +Only four methods need to be implemented: + +- addOperator(String operatorString, OperatorIfc operator) - which allows to add an operator. +- OperatorIfc getPrefixOperator(String operatorString) - retrieves a prefix operator. +- OperatorIfc getPostfixOperator(String operatorString) - retrieves a postfix operator. +- OperatorIfc getInfixOperator(String operatorString) - retrieves a infix operator. + +The _addOperator()_ method is only called when an operator is added to the configuration, using the +_withAdditionalOperators()_ in the _ExpressionConfiguration_ object. + +> NOTE: The operator names that are passed to the methods are case-sensitive, just like they were +> entered in the expression. + +The custom operator dictionary can then be specified in the expression configuration: + +```java +ExpressionConfiguration configuration = ExpressionConfiguration.builder() + .operatorDictionary(new MyOperatorDictionary()) + .build(); +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..243335f --- /dev/null +++ b/docs/index.md @@ -0,0 +1,131 @@ +--- +layout: default +title: Welcome +nav_order: 1 +--- + +# EvalEx - Java Expression Evaluator + +EvalEx is a handy expression evaluator for Java, that allows to parse and evaluate expression +strings. + +## Key Features: + +- Supports numerical, boolean, string, array and structure expressions, operations and variables. +- Array and structure support: Arrays and structures can be mixed, building arbitrary data + structures. +- Uses BigDecimal for numerical calculations. +- MathContext and number of decimal places can be configured, with optional automatic rounding. +- No dependencies to external libraries. +- Easy integration into existing systems to access data. +- Predefined boolean and mathematical operators. +- Predefined mathematical, boolean and string functions. +- Custom functions and operators can be added. +- Functions can be defined with a variable number of arguments (see MIN, MAX and SUM functions). +- Supports hexadecimal and scientific notations of numbers. +- Supports implicit multiplication, e.g. (a+b)(a-b) or 2(x-y) which equals to (a+b)\*(a-b) or 2\*( + x-y) +- Lazy evaluation of function parameters (see the IF function) and support of sub-expressions. + +## Examples + +A simple example, that shows how it works in general: + +```java +Expression expression = new Expression("1 + 2 / (4 * SQRT(4))"); + +EvaluationValue result = expression.evaluate(); + +System.out.println(result.getNumberValue()); // prints 1.25 +``` + +Of course, variables can be specified in the expression and their values can be passed for +evaluation: + +```java +Expression expression = new Expression("(a + b) * (a - b)"); + +EvaluationValue result = expression + .with("a", 3.5) + .and("b", 2.5) + .evaluate(); + +System.out.println(result.getNumberValue()); // prints 6.00 +``` + +Boolean expressions produce a boolean result: + +```java +Expression expression = new Expression("level > 2 || level <= 0"); + +EvaluationValue result = expression + .with("level", 3.5) + .evaluate(); + +System.out.println(result.getBooleanValue()); // prints true +``` + +Like in Java, strings and text can be mixed: + +```java +Expression expression = new Expression("\"Hello \" + name + \", you are \" + age") + .with("name","Frank") + .and("age",38); + +System.out.println(expression.evaluate().getStringValue()); // prints Hello Frank, you are 38 +``` + +Arrays are supported and can be passed as Java _Lists_: + +```java +Expression expression = new Expression("values[i-1] * factors[i-1]"); + +EvaluationValue result = expression + .with("values", List.of(2, 3, 4)) + .and("factors", List.of(2, 4, 6)) + .and("i", 1) + .evaluate(); + +System.out.println(result.getNumberValue()); // prints 4 +``` + +Structures are supported and can be passed as Java _Maps_. +Arrays and Structures can be combined to build arbitrary data structures: + +```java +Map order = new HashMap<>(); +order.put("id", 12345); +order.put("name", "Mary"); + +Map position = new HashMap<>(); +position.put("article", 3114); +position.put("amount", 3); +position.put("price", new BigDecimal("14.95")); + +order.put("positions", List.of(position)); + +Expression expression = new Expression("order.positions[x].amount * order.positions[x].price") + .with("order", order) + .and("x", 0); + +BigDecimal result = expression.evaluate().getNumberValue(); + +System.out.println(result); // prints 44.85 +``` + +## Author and License + +Copyright 2012-2022 by Udo Klimaschewski + +**Thanks to all who contributed to this +project: [Contributors](https://github.com/ezylang/EvalEx/graphs/contributors)** + +The software is licensed under the Apache License, Version 2.0 ( +see [LICENSE](https://raw.githubusercontent.com/ezylang/EvalEx/main/LICENSE) file). + +* The *power of* operator (^) implementation was copied + from [Stack Overflow](http://stackoverflow.com/questions/3579779/how-to-do-a-fractional-power-on-bigdecimal-in-java) + Thanks to Gene Marin +* The SQRT() function implementation was taken from the + book [The Java Programmers Guide To numerical Computing](http://www.amazon.de/Java-Number-Cruncher-Programmers-Numerical/dp/0130460419) ( + Ronald Mak, 2002) diff --git a/docs/references/constants.md b/docs/references/constants.md new file mode 100644 index 0000000..01f5021 --- /dev/null +++ b/docs/references/constants.md @@ -0,0 +1,19 @@ +--- +layout: default +title: Standard Constants +nav_order: 1 +parent: References +--- + +### Standard Constants + +Available through the _ExpressionConfiguration.StandardConstants_ constant: + +| Name | Value | +|-------|---------------------------------------------------------------| +| TRUE | Boolean.TRUE | +| FALSE | Boolean.FALSE | +| PI | 3.14159265358979323846264338327950288419716939937510582097... | +| E | 2.71828182845904523536028747135266249775724709369995957496... | + + diff --git a/docs/references/functions.md b/docs/references/functions.md new file mode 100644 index 0000000..6a5cf10 --- /dev/null +++ b/docs/references/functions.md @@ -0,0 +1,76 @@ +--- +layout: default +title: Standard Functions +nav_order: 3 +parent: References +--- + +## Standard Functions + +Available through the _ExpressionConfiguration.StandardFunctionsDictionary_ constant: + +### Basic Functions + +| Name | Value | +|--------------------------------------------|-------------------------------------------------------------------------------------------------------------------------| +| ABS(value) | Absolute (non-negative) value | +| CEILING(value) | Rounds the given value an integer using the rounding mode CEILING | +| FACT(base) | Calculates the factorial of a base value | +| FLOOR(value) | Rounds the given value an integer using the rounding mode FLOOR | +| IF(condition, resultIfTrue, resultIfFalse) | Conditional evaluation function. If _condition_ is true, the _resultIfTrue_ is returned, else the _resultIfFalse_ value | +| LOG(value) | The natural logarithm (base e) of a value | +| LOG10(value) | The base 10 logarithm of a value | +| MAX(value, ...) | Returns the maximum value of all parameters | +| MIN(value, ...) | Returns the maximum value of all parameters | +| NOT(value) | Boolean negation, implemented as a function (for compatibility) | +| RANDOM() | Produces a random value between 0 and 1 | +| ROUND(value, scale) | Rounds the given value to the specified scale, using the current rounding mode | +| SQRT(value) | Square root function | +| SUM(value, ...) | Returns the sum of all parameters | + +### String Functions + +| Name | Value | +|---------------------------------|-----------------------------------------------------------------------| +| STR_CONTAINS(string, substring) | Returns true, if the string contains the substring (case-insensitive) | +| STR_LOWER(value) | Converts the given value to lower case | +| STR_UPPER(value) | Converts the given value to upper case | + +### trigonometric Functions + +| Name | Value | +|--------------|------------------------------------------------------------------------------------------------| +| ACOS(value) | Returns the the arc-cosine (in degrees) | +| ACOSH(value) | Returns the the hyperbolic arc-cosine (in degrees) | +| ACOSR(value) | Returns the the arc-cosine (in radians) | +| ACOT(value) | Returns the the arc-co-tangent (in degrees) | +| ACOTR(value) | Returns the the arc-co-tangent (in radians) | +| ASIN(value) | Returns the the arc-sine (in degrees) | +| ASINH(value) | Returns the the hyperbolic arc-sine (in degrees) | +| ASINR(value) | Returns the the arc-sine (in radians) | +| ATAN(value) | Returns the the arc-tangent (in degrees) | +| ATAN(value) | Returns the the arc-tangent (in degrees) | +| ATAN2(y, x) | Returns the angle of atan2 (in degrees) | +| ATAN2R(y, x) | Returns the angle of atan2 (in radians) | +| ATANR(value) | Returns the the arc-tangent (in radians) | +| COS(value) | Returns the cosine of an angle (in degrees) | +| COSH(value) | Returns the hyperbolic cosine of a value | +| COSR(value) | Returns the cosine of an angle (in radians) | +| COT(value) | Returns the co-tangent of an angle (in degrees) | +| COTH(value) | Returns the hyperbolic co-tangent of a value | +| COTR(value) | Returns the co-tangent of an angle (in radians) | +| CSC(value) | Returns the co-secant of an angle (in degrees) | +| CSCH(value) | Returns the hyperbolic co-secant of a value | +| CSCR(value) | Returns the co-secant of an angle (in radians) | +| DEG(rad) | Converts an angle measured in radians to an approximately equivalent angle measured in degrees | +| RAD(degrees) | Converts an angle measured in degrees to an approximately equivalent angle measured in radians | +| SEC(value) | Returns the secant of an angle (in degrees) | +| SECH(value) | Returns the hyperbolic secant of an angle (in degrees) | +| SECR(value) | Returns the secant of an angle (in radians) | +| SIN(value) | Returns the sine of an angle (in degrees) | +| SINH(value) | Returns the hyperbolic sine of a value | +| SINR(value) | Returns the sine of an angle (in radians) | +| TAN(value) | Returns the tangent of an angle (in degrees) | +| TANH(value) | Returns the hyperbolic tangent of a value | +| TANR(value) | Returns the tangent of an angle (in radians) | + diff --git a/docs/references/operators.md b/docs/references/operators.md new file mode 100644 index 0000000..1181c77 --- /dev/null +++ b/docs/references/operators.md @@ -0,0 +1,38 @@ +--- +layout: default +title: Standard Operators +nav_order: 2 +parent: References +--- + +## Standard Operators + +Available through the _ExpressionConfiguration.StandardOperatorsDictionary_ constant: + +### Arithmetic operators + +| Name | Value | +|------|-----------------------------------------| +| - | The prefix minus operator, like in "-2" | +| + | The prefix minus operator, like in "+2" | +| - | The infix minus operator, like in "5-2" | +| + | The infix plus operator, like in "5+2" | +| * | The multiplication operator | +| / | The division operator | +| ^ | The power-of operator | +| % | The modulo operator (remainder) | + +### Boolean operators + +| Name | Value | +|--------------|-------------------------------------| +| =, == | The equals operator | +| !=, <> | The not equals operator | +| ! | The prefix not operator, like in !a | +| > | The greater than operator | +| >= | The greater equals operator | +| < | The less than operator | +| <= | The less equals operator | +| && | The and operator | +| || | The or operator | + diff --git a/docs/references/references.md b/docs/references/references.md new file mode 100644 index 0000000..9cd9df5 --- /dev/null +++ b/docs/references/references.md @@ -0,0 +1,6 @@ +--- +layout: default +title: References +nav_order: 9999 +has_children: true +--- diff --git a/pom.xml b/pom.xml index 05bebfe..25d1921 100644 --- a/pom.xml +++ b/pom.xml @@ -3,25 +3,24 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - com.udojava + com.ezylang EvalEx - 2.8-SNAPSHOT + 3.0.0-SNAPSHOT - EvalEx - EvalEx is a handy expression evaluator for Java, that allows to evaluate simple - mathematical and boolean expressions. + EvalEx is a handy expression evaluator for Java, that allows to evaluate + expressions. - https://github.com/uklimaschewski/EvalEx + https://github.com/ezylang/EvalEx Udo Klimaschewski - http://www.udojava.com + https://github.com/ezylang/EvalEx - MIT License - http://www.opensource.org/licenses/mit-license.php + Apache License 2.0 + https://github.com/ezylang/EvalEx/blob/main/LICENSE @@ -33,36 +32,55 @@ - scm:git:git@github.com:uklimaschewski/EvalEx.git - scm:git:git@github.com:uklimaschewski/EvalEx.git - https://github.com/uklimaschewski/EvalEx + scm:git:git@github.com:ezylang/EvalEx.git + scm:git:git@github.com:ezylang/EvalEx.git + https://github.com/ezylang/EvalEx github - https://github.com/uklimaschewski/EvalEx/issues + https://github.com/ezylang/EvalEx/issues - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - UTF-8 + 5.9.0 + 11 + 11 + + + + org.projectlombok + lombok + 1.18.24 + provided + + + + org.junit.jupiter + junit-jupiter-engine + ${version.junit} + test + + + org.junit.jupiter + junit-jupiter-params + ${version.junit} + test + + + org.assertj + assertj-core + 3.23.1 + test + - junit - junit - 4.13.1 + org.mockito + mockito-core + 4.6.1 test @@ -70,136 +88,71 @@ - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 + com.diffplug.spotless + spotless-maven-plugin + 2.23.0 + + + validate + + check + + + - 1.6 - 1.6 + + + src/main/java/**/*.java + src/test/java/**/*.java + + + 1.15.0 + + true + + + ${basedir}/spotless/header.txt + + - org.moditect - moditect-maven-plugin - 1.0.0.RC2 + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + org.junit.platform + junit-platform-surefire-provider + 1.2.0 + + + + + org.jacoco + jacoco-maven-plugin + 0.8.5 - add-module-info - package + prepare-agent - add-module-info + prepare-agent + + + + report + prepare-package + + report - - 9 - true - - - ${project.basedir}/src/main/java9/module-info.java - - - - - - - coverage - - - - org.jacoco - jacoco-maven-plugin - 0.8.5 - - - prepare-agent - - prepare-agent - - - - report - test - - report - - - - - - - - - - publish - - - - - maven-source-plugin - 3.0.1 - - - attach-sources - package - - jar-no-fork - - - - - - - maven-javadoc-plugin - 3.0.0-M1 - - none - false - - - - attach-javadocs - package - - jar - - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - - sign-artifacts - verify - - sign - - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.8 - true - - ossrh - https://oss.sonatype.org/ - true - - - - - - - - diff --git a/spotless/header.txt b/spotless/header.txt new file mode 100644 index 0000000..1418e1f --- /dev/null +++ b/spotless/header.txt @@ -0,0 +1,15 @@ +/* + Copyright 2012-$YEAR Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ diff --git a/src/main/java/com/ezylang/evalex/BaseException.java b/src/main/java/com/ezylang/evalex/BaseException.java new file mode 100644 index 0000000..f1c90d5 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/BaseException.java @@ -0,0 +1,39 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** Base exception class used in EvalEx. */ +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +@ToString +public class BaseException extends Exception { + + @Getter @EqualsAndHashCode.Include private final int startPosition; + @Getter @EqualsAndHashCode.Include private final int endPosition; + @Getter @EqualsAndHashCode.Include private final String tokenString; + @Getter @EqualsAndHashCode.Include private final String message; + + public BaseException(int startPosition, int endPosition, String tokenString, String message) { + super(message); + this.startPosition = startPosition; + this.endPosition = endPosition; + this.tokenString = tokenString; + this.message = super.getMessage(); + } +} diff --git a/src/main/java/com/ezylang/evalex/EvaluationException.java b/src/main/java/com/ezylang/evalex/EvaluationException.java new file mode 100644 index 0000000..c99d6a6 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/EvaluationException.java @@ -0,0 +1,34 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import com.ezylang.evalex.parser.Token; + +/** Exception while evaluating the parsed expression. */ +public class EvaluationException extends BaseException { + + public EvaluationException(Token token, String message) { + super( + token.getStartPosition(), + token.getStartPosition() + token.getValue().length(), + token.getValue(), + message); + } + + public static EvaluationException ofUnsupportedDataTypeInOperation(Token token) { + return new EvaluationException(token, "Unsupported data types in operation"); + } +} diff --git a/src/main/java/com/ezylang/evalex/Expression.java b/src/main/java/com/ezylang/evalex/Expression.java new file mode 100644 index 0000000..af36f59 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/Expression.java @@ -0,0 +1,342 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.DataAccessorIfc; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.FunctionIfc; +import com.ezylang.evalex.parser.ASTNode; +import com.ezylang.evalex.parser.ParseException; +import com.ezylang.evalex.parser.ShuntingYardConverter; +import com.ezylang.evalex.parser.Token; +import com.ezylang.evalex.parser.Tokenizer; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import lombok.Getter; + +/** + * Main class that allow creating, parsing, passing parameters and evaluating an expression string. + * + * @see EvalEx Homepage + */ +public class Expression { + @Getter private final ExpressionConfiguration configuration; + + @Getter private final String expressionString; + + @Getter private final DataAccessorIfc dataAccessor; + + private ASTNode abstractSyntaxTree; + + /** + * Creates a new expression with the default configuration. The expression is not parsed until it + * is first evaluated or validated. + * + * @param expressionString A string holding an expression. + */ + public Expression(String expressionString) { + this(expressionString, ExpressionConfiguration.defaultConfiguration()); + } + + /** + * Creates a new expression with a custom configuration. The expression is not parsed until it is + * first evaluated or validated. + * + * @param expressionString A string holding an expression. + */ + public Expression(String expressionString, ExpressionConfiguration configuration) { + this.expressionString = expressionString; + this.configuration = configuration; + this.dataAccessor = configuration.getDataAccessorSupplier().get(); + + // add default constants to data + for (Map.Entry constant : + configuration.getDefaultConstants().entrySet()) { + getDataAccessor().setData(constant.getKey(), constant.getValue()); + } + } + + /** + * Evaluates the expression by parsing it (if not done before) and the evaluating it. + * + * @return The evaluation result value. + * @throws EvaluationException If there were problems while evaluating the expression. + * @throws ParseException If there were problems while parsing the expression. + */ + public EvaluationValue evaluate() throws EvaluationException, ParseException { + return evaluateSubtree(getAbstractSyntaxTree()); + } + + /** + * Evaluates only a subtree of the abstract syntax tree. + * + * @param startNode The {@link ASTNode} to start evaluation from. + * @return The evaluation result value. + * @throws EvaluationException If there were problems while evaluating the expression. + */ + public EvaluationValue evaluateSubtree(ASTNode startNode) throws EvaluationException { + Token token = startNode.getToken(); + EvaluationValue result; + switch (token.getType()) { + case NUMBER_LITERAL: + result = EvaluationValue.numberOfString(token.getValue(), configuration.getMathContext()); + break; + case STRING_LITERAL: + result = new EvaluationValue(token.getValue()); + break; + case VARIABLE_OR_CONSTANT: + result = getDataAccessor().getData(token.getValue()); + if (result.isExpressionNode()) { + result = evaluateSubtree(result.getExpressionNode()); + } + break; + case PREFIX_OPERATOR: + case POSTFIX_OPERATOR: + result = + token + .getOperatorDefinition() + .evaluate(this, token, evaluateSubtree(startNode.getParameters().get(0))); + break; + case INFIX_OPERATOR: + result = + token + .getOperatorDefinition() + .evaluate( + this, + token, + evaluateSubtree(startNode.getParameters().get(0)), + evaluateSubtree(startNode.getParameters().get(1))); + break; + case ARRAY_INDEX: + result = evaluateArrayIndex(startNode); + break; + case STRUCTURE_SEPARATOR: + result = evaluateStructureSeparator(startNode); + break; + case FUNCTION: + result = evaluateFunction(startNode, token); + break; + default: + throw new EvaluationException(token, "Unexpected evaluation token: " + token); + } + + return result.isNumberValue() ? roundAndStripZerosIfNeeded(result) : result; + } + + private EvaluationValue evaluateFunction(ASTNode startNode, Token token) + throws EvaluationException { + List parameterResults = new ArrayList<>(); + for (int i = 0; i < startNode.getParameters().size(); i++) { + if (token.getFunctionDefinition().isParameterLazy(i)) { + parameterResults.add(new EvaluationValue(startNode.getParameters().get(i))); + } else { + parameterResults.add(evaluateSubtree(startNode.getParameters().get(i))); + } + } + + EvaluationValue[] parameters = parameterResults.toArray(new EvaluationValue[0]); + + FunctionIfc function = token.getFunctionDefinition(); + + function.validatePreEvaluation(token, parameters); + + return function.evaluate(this, token, parameters); + } + + private EvaluationValue evaluateArrayIndex(ASTNode startNode) throws EvaluationException { + EvaluationValue array = evaluateSubtree(startNode.getParameters().get(0)); + EvaluationValue index = evaluateSubtree(startNode.getParameters().get(1)); + + if (array.isArrayValue() && index.isNumberValue()) { + return array.getArrayValue().get(index.getNumberValue().intValue()); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(startNode.getToken()); + } + } + + private EvaluationValue evaluateStructureSeparator(ASTNode startNode) throws EvaluationException { + EvaluationValue structure = evaluateSubtree(startNode.getParameters().get(0)); + String name = startNode.getParameters().get(1).getToken().getValue(); + + if (structure.isStructureValue()) { + return structure.getStructureValue().get(name); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(startNode.getToken()); + } + } + + /** + * Rounds the given value, if the decimal places are configured. Also strips trailing decimal + * zeros, if configured. + * + * @param value The input value. + * @return The rounded value, or the input value if rounding is not configured or possible. + */ + private EvaluationValue roundAndStripZerosIfNeeded(EvaluationValue value) { + BigDecimal bigDecimal = value.getNumberValue(); + if (configuration.getDecimalPlacesRounding() + != ExpressionConfiguration.DECIMAL_PLACES_ROUNDING_UNLIMITED) { + bigDecimal = + bigDecimal.setScale( + configuration.getDecimalPlacesRounding(), + configuration.getMathContext().getRoundingMode()); + } + if (configuration.isStripTrailingZeros()) { + bigDecimal = bigDecimal.stripTrailingZeros(); + } + return new EvaluationValue(bigDecimal); + } + + /** + * Returns the root ode of the parsed abstract syntax tree. + * + * @return The abstract syntax tree root node. + * @throws ParseException If there were problems while parsing the expression. + */ + public ASTNode getAbstractSyntaxTree() throws ParseException { + if (abstractSyntaxTree == null) { + Tokenizer tokenizer = new Tokenizer(expressionString, configuration); + ShuntingYardConverter converter = + new ShuntingYardConverter(expressionString, tokenizer.parse(), configuration); + abstractSyntaxTree = converter.toAbstractSyntaxTree(); + } + + return abstractSyntaxTree; + } + + /** + * Validates the expression by parsing it and throwing an exception, if the parser fails. + * + * @throws ParseException If there were problems while parsing the expression. + */ + public void validate() throws ParseException { + getAbstractSyntaxTree(); + } + + /** + * Adds a variable value to the expression data storage. If a value with the same name already + * exists, it is overridden. The data type will be determined by examining the passed value + * object. An exception is thrown, if he found data type is not supported. + * + * @param variable The variable name. + * @param value The variable value. + * @return The Expression instance, to allow chaining of methods. + */ + public Expression with(String variable, Object value) { + getDataAccessor().setData(variable, new EvaluationValue(value)); + return this; + } + + /** + * Adds a variable value to the expression data storage. If a value with the same name already + * exists, it is overridden. The data type will be determined by examining the passed value + * object. An exception is thrown, if he found data type is not supported. + * + * @param variable The variable name. + * @param value The variable value. + * @return The Expression instance, to allow chaining of methods. + */ + public Expression and(String variable, Object value) { + return with(variable, value); + } + + /** + * Adds all variables values defined in the map with their name (key) and value to the data + * storage.If a value with the same name already exists, it is overridden. The data type will be + * determined by examining the passed value object. An exception is thrown, if he found data type + * is not supported. + * + * @param values A map with variable values. + * @return The Expression instance, to allow chaining of methods. + */ + public Expression withValues(Map values) { + for (Map.Entry entry : values.entrySet()) { + getDataAccessor().setData(entry.getKey(), new EvaluationValue(entry.getValue())); + } + return this; + } + + /** + * Create an AST representation for an expression string. The node can then be used as a + * sub-expression. Subexpressions are not cached. + * + * @param expression The expression string. + * @return The root node of the expression AST representation. + * @throws ParseException On any parsing error. + */ + public ASTNode createExpressionNode(String expression) throws ParseException { + Tokenizer tokenizer = new Tokenizer(expression, configuration); + ShuntingYardConverter converter = + new ShuntingYardConverter(expression, tokenizer.parse(), configuration); + return converter.toAbstractSyntaxTree(); + } + + /** + * Converts a double value to an {@link EvaluationValue} by considering the configured {@link + * java.math.MathContext}. + * + * @param value The double value to covert. + * @return An {@link EvaluationValue} of type {@link EvaluationValue.DataType#NUMBER}. + */ + public EvaluationValue convertDoubleValue(double value) { + return new EvaluationValue(value, configuration.getMathContext()); + } + + /** + * Returns the list of all nodes of the abstract syntax tree. + * + * @return The list of all nodes in the parsed expression. + * @throws ParseException If there were problems while parsing the expression. + */ + public List getAllASTNodes() throws ParseException { + return getAllASTNodesForNode(getAbstractSyntaxTree()); + } + + private List getAllASTNodesForNode(ASTNode node) { + List nodes = new ArrayList<>(); + nodes.add(node); + for (ASTNode child : node.getParameters()) { + nodes.addAll(getAllASTNodesForNode(child)); + } + return nodes; + } + + /** + * Returns all variables or constants that are used i the expression, excluding the standard + * constants like e.g. PI or TRUE and FALSE. + * + * @return All used variables and constants less the standard constants. + * @throws ParseException If there were problems while parsing the expression. + */ + public Set getUsedVariables() throws ParseException { + Set variables = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + + for (ASTNode node : getAllASTNodes()) { + if (node.getToken().getType() == Token.TokenType.VARIABLE_OR_CONSTANT + && !configuration.getDefaultConstants().containsKey(node.getToken().getValue()) + && !variables.contains(node.getToken().getValue())) { + variables.add(node.getToken().getValue()); + } + } + + return variables; + } +} diff --git a/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java b/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java new file mode 100644 index 0000000..e48128a --- /dev/null +++ b/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java @@ -0,0 +1,345 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.config; + +import com.ezylang.evalex.data.DataAccessorIfc; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.data.MapBasedDataAccessor; +import com.ezylang.evalex.functions.FunctionIfc; +import com.ezylang.evalex.functions.basic.AbsFunction; +import com.ezylang.evalex.functions.basic.CeilingFunction; +import com.ezylang.evalex.functions.basic.FactFunction; +import com.ezylang.evalex.functions.basic.FloorFunction; +import com.ezylang.evalex.functions.basic.IfFunction; +import com.ezylang.evalex.functions.basic.Log10Function; +import com.ezylang.evalex.functions.basic.LogFunction; +import com.ezylang.evalex.functions.basic.MaxFunction; +import com.ezylang.evalex.functions.basic.MinFunction; +import com.ezylang.evalex.functions.basic.NotFunction; +import com.ezylang.evalex.functions.basic.RandomFunction; +import com.ezylang.evalex.functions.basic.RoundFunction; +import com.ezylang.evalex.functions.basic.SqrtFunction; +import com.ezylang.evalex.functions.basic.SumFunction; +import com.ezylang.evalex.functions.string.StringContains; +import com.ezylang.evalex.functions.string.StringLowerFunction; +import com.ezylang.evalex.functions.string.StringUpperFunction; +import com.ezylang.evalex.functions.trigonometric.AcosFunction; +import com.ezylang.evalex.functions.trigonometric.AcosHFunction; +import com.ezylang.evalex.functions.trigonometric.AcosRFunction; +import com.ezylang.evalex.functions.trigonometric.AcotFunction; +import com.ezylang.evalex.functions.trigonometric.AcotRFunction; +import com.ezylang.evalex.functions.trigonometric.AsinFunction; +import com.ezylang.evalex.functions.trigonometric.AsinHFunction; +import com.ezylang.evalex.functions.trigonometric.AsinRFunction; +import com.ezylang.evalex.functions.trigonometric.Atan2Function; +import com.ezylang.evalex.functions.trigonometric.Atan2RFunction; +import com.ezylang.evalex.functions.trigonometric.AtanFunction; +import com.ezylang.evalex.functions.trigonometric.AtanHFunction; +import com.ezylang.evalex.functions.trigonometric.AtanRFunction; +import com.ezylang.evalex.functions.trigonometric.CosFunction; +import com.ezylang.evalex.functions.trigonometric.CosHFunction; +import com.ezylang.evalex.functions.trigonometric.CosRFunction; +import com.ezylang.evalex.functions.trigonometric.CotFunction; +import com.ezylang.evalex.functions.trigonometric.CotHFunction; +import com.ezylang.evalex.functions.trigonometric.CotRFunction; +import com.ezylang.evalex.functions.trigonometric.CscFunction; +import com.ezylang.evalex.functions.trigonometric.CscHFunction; +import com.ezylang.evalex.functions.trigonometric.CscRFunction; +import com.ezylang.evalex.functions.trigonometric.DegFunction; +import com.ezylang.evalex.functions.trigonometric.RadFunction; +import com.ezylang.evalex.functions.trigonometric.SecFunction; +import com.ezylang.evalex.functions.trigonometric.SecHFunction; +import com.ezylang.evalex.functions.trigonometric.SecRFunction; +import com.ezylang.evalex.functions.trigonometric.SinFunction; +import com.ezylang.evalex.functions.trigonometric.SinHFunction; +import com.ezylang.evalex.functions.trigonometric.SinRFunction; +import com.ezylang.evalex.functions.trigonometric.TanFunction; +import com.ezylang.evalex.functions.trigonometric.TanHFunction; +import com.ezylang.evalex.functions.trigonometric.TanRFunction; +import com.ezylang.evalex.operators.OperatorIfc; +import com.ezylang.evalex.operators.arithmetic.InfixDivisionOperator; +import com.ezylang.evalex.operators.arithmetic.InfixMinusOperator; +import com.ezylang.evalex.operators.arithmetic.InfixModuloOperator; +import com.ezylang.evalex.operators.arithmetic.InfixMultiplicationOperator; +import com.ezylang.evalex.operators.arithmetic.InfixPlusOperator; +import com.ezylang.evalex.operators.arithmetic.InfixPowerOfOperator; +import com.ezylang.evalex.operators.arithmetic.PrefixMinusOperator; +import com.ezylang.evalex.operators.arithmetic.PrefixPlusOperator; +import com.ezylang.evalex.operators.booleans.InfixAndOperator; +import com.ezylang.evalex.operators.booleans.InfixEqualsOperator; +import com.ezylang.evalex.operators.booleans.InfixGreaterEqualsOperator; +import com.ezylang.evalex.operators.booleans.InfixGreaterOperator; +import com.ezylang.evalex.operators.booleans.InfixLessEqualsOperator; +import com.ezylang.evalex.operators.booleans.InfixLessOperator; +import com.ezylang.evalex.operators.booleans.InfixNotEqualsOperator; +import com.ezylang.evalex.operators.booleans.InfixOrOperator; +import com.ezylang.evalex.operators.booleans.PrefixNotOperator; +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Supplier; +import lombok.Builder; +import lombok.Getter; + +/** + * The expression configuration can be used to configure various aspects of expression parsing and + * evaluation.
+ * A Builder is provided to create custom configurations, e.g.:
+ * + *
+ *   ExpressionConfiguration config = ExpressionConfiguration.builder().mathContext(MathContext.DECIMAL32).arraysAllowed(false).build();
+ * 
+ * + *
+ * Additional operators and functions can be added to an existing configuration:
+ * + *
+ *     ExpressionConfiguration.defaultConfiguration()
+ *        .withAdditionalOperators(
+ *            Map.entry("++", new PrefixPlusPlusOperator()),
+ *            Map.entry("++", new PostfixPlusPlusOperator()))
+ *        .withAdditionalFunctions(Map.entry("save", new SaveFunction()),
+ *            Map.entry("update", new UpdateFunction()));
+ * 
+ */ +@Builder +public class ExpressionConfiguration { + + /** The standard set constants for EvalEx. */ + public static final Map StandardConstants = + Collections.unmodifiableMap(getStandardConstants()); + + /** Setting the decimal places to unlimited, will disable intermediate rounding. */ + public static final int DECIMAL_PLACES_ROUNDING_UNLIMITED = -1; + + /** The default math context has a precision of 68 and {@link RoundingMode#HALF_EVEN}. */ + public static final MathContext DEFAULT_MATH_CONTEXT = + new MathContext(68, RoundingMode.HALF_EVEN); + + /** The operator dictionary holds all operators that will be allowed in an expression. */ + @Builder.Default + @Getter + @SuppressWarnings("unchecked") + private final OperatorDictionaryIfc operatorDictionary = + MapBasedOperatorDictionary.ofOperators( + // arithmetic + Map.entry("+", new PrefixPlusOperator()), + Map.entry("-", new PrefixMinusOperator()), + Map.entry("+", new InfixPlusOperator()), + Map.entry("-", new InfixMinusOperator()), + Map.entry("*", new InfixMultiplicationOperator()), + Map.entry("/", new InfixDivisionOperator()), + Map.entry("^", new InfixPowerOfOperator()), + Map.entry("%", new InfixModuloOperator()), + // booleans + Map.entry("=", new InfixEqualsOperator()), + Map.entry("==", new InfixEqualsOperator()), + Map.entry("!=", new InfixNotEqualsOperator()), + Map.entry("<>", new InfixNotEqualsOperator()), + Map.entry(">", new InfixGreaterOperator()), + Map.entry(">=", new InfixGreaterEqualsOperator()), + Map.entry("<", new InfixLessOperator()), + Map.entry("<=", new InfixLessEqualsOperator()), + Map.entry("&&", new InfixAndOperator()), + Map.entry("||", new InfixOrOperator()), + Map.entry("!", new PrefixNotOperator())); + + /** The function dictionary holds all functions that will be allowed in an expression. */ + @Builder.Default + @Getter + @SuppressWarnings("unchecked") + private final FunctionDictionaryIfc functionDictionary = + MapBasedFunctionDictionary.ofFunctions( + // basic functions + Map.entry("ABS", new AbsFunction()), + Map.entry("CEILING", new CeilingFunction()), + Map.entry("FACT", new FactFunction()), + Map.entry("FLOOR", new FloorFunction()), + Map.entry("IF", new IfFunction()), + Map.entry("LOG", new LogFunction()), + Map.entry("LOG10", new Log10Function()), + Map.entry("MAX", new MaxFunction()), + Map.entry("MIN", new MinFunction()), + Map.entry("NOT", new NotFunction()), + Map.entry("RANDOM", new RandomFunction()), + Map.entry("ROUND", new RoundFunction()), + Map.entry("SUM", new SumFunction()), + Map.entry("SQRT", new SqrtFunction()), + // trigonometric + Map.entry("ACOS", new AcosFunction()), + Map.entry("ACOSH", new AcosHFunction()), + Map.entry("ACOSR", new AcosRFunction()), + Map.entry("ACOT", new AcotFunction()), + Map.entry("ACOTR", new AcotRFunction()), + Map.entry("ASIN", new AsinFunction()), + Map.entry("ASINH", new AsinHFunction()), + Map.entry("ASINR", new AsinRFunction()), + Map.entry("ATAN", new AtanFunction()), + Map.entry("ATAN2", new Atan2Function()), + Map.entry("ATAN2R", new Atan2RFunction()), + Map.entry("ATANH", new AtanHFunction()), + Map.entry("ATANR", new AtanRFunction()), + Map.entry("COS", new CosFunction()), + Map.entry("COSH", new CosHFunction()), + Map.entry("COSR", new CosRFunction()), + Map.entry("COT", new CotFunction()), + Map.entry("COTH", new CotHFunction()), + Map.entry("COTR", new CotRFunction()), + Map.entry("CSC", new CscFunction()), + Map.entry("CSCH", new CscHFunction()), + Map.entry("CSCR", new CscRFunction()), + Map.entry("DEG", new DegFunction()), + Map.entry("RAD", new RadFunction()), + Map.entry("SIN", new SinFunction()), + Map.entry("SINH", new SinHFunction()), + Map.entry("SINR", new SinRFunction()), + Map.entry("SEC", new SecFunction()), + Map.entry("SECH", new SecHFunction()), + Map.entry("SECR", new SecRFunction()), + Map.entry("TAN", new TanFunction()), + Map.entry("TANH", new TanHFunction()), + Map.entry("TANR", new TanRFunction()), + // string functions + Map.entry("STR_CONTAINS", new StringContains()), + Map.entry("STR_LOWER", new StringLowerFunction()), + Map.entry("STR_UPPER", new StringUpperFunction())); + + /** The math context to use. */ + @Builder.Default @Getter private final MathContext mathContext = DEFAULT_MATH_CONTEXT; + + /** + * The data accessor is responsible for accessing variable and constant values in an expression. + * The supplier will be called once for each new expression, the default is to create a new {@link + * MapBasedDataAccessor} instance for each expression, providing a new storage for each + * expression. + */ + @Builder.Default @Getter + private final Supplier dataAccessorSupplier = MapBasedDataAccessor::new; + + /** + * Default constants will be added automatically to each expression and can be used in expression + * evaluation. + */ + @Builder.Default @Getter + private final Map defaultConstants = getStandardConstants(); + + /** Support for arrays in expressions are allowed or not. */ + @Builder.Default @Getter private final boolean arraysAllowed = true; + + /** Support for structures in expressions are allowed or not. */ + @Builder.Default @Getter private final boolean structuresAllowed = true; + + /** Support for implicit multiplication, like in (a+b)(b+c) are allowed or not. */ + @Builder.Default @Getter private final boolean implicitMultiplicationAllowed = true; + + /** + * The power of operator precedence, can be set higher {@link + * OperatorIfc#OPERATOR_PRECEDENCE_POWER_HIGHER} or to a custom value. + */ + @Builder.Default @Getter + private final int powerOfPrecedence = OperatorIfc.OPERATOR_PRECEDENCE_POWER; + + /** + * If specified, all results from operations and functions will be rounded to the specified number + * of decimal digits, using the MathContexts rounding mode. + */ + @Builder.Default @Getter + private final int decimalPlacesRounding = DECIMAL_PLACES_ROUNDING_UNLIMITED; + + /** + * If set to true (default), then the trailing decimal zeros in a number result will be stripped. + */ + @Builder.Default @Getter private final boolean stripTrailingZeros = true; + + /** + * Convenience method to create a default configuration. + * + * @return A configuration with default settings. + */ + public static ExpressionConfiguration defaultConfiguration() { + return ExpressionConfiguration.builder().build(); + } + + /** + * Adds additional operators to this configuration. + * + * @param operators variable number of arguments with a map entry holding the operator name and + * implementation.
+ * Example: + *
+   *                                                        ExpressionConfiguration.defaultConfiguration()
+   *                                                           .withAdditionalOperators(
+   *                                                               Map.entry("++", new PrefixPlusPlusOperator()),
+   *                                                               Map.entry("++", new PostfixPlusPlusOperator()));
+   *                                                        
+ * + * @return The modified configuration, to allow chaining of methods. + */ + @SafeVarargs + public final ExpressionConfiguration withAdditionalOperators( + Map.Entry... operators) { + Arrays.stream(operators) + .forEach(entry -> operatorDictionary.addOperator(entry.getKey(), entry.getValue())); + return this; + } + + /** + * Adds additional functions to this configuration. + * + * @param functions variable number of arguments with a map entry holding the functions name and + * implementation.
+ * Example: + *
+   *                                                        ExpressionConfiguration.defaultConfiguration()
+   *                                                           .withAdditionalFunctions(
+   *                                                               Map.entry("save", new SaveFunction()),
+   *                                                               Map.entry("update", new UpdateFunction()));
+   *                                                        
+ * + * @return The modified configuration, to allow chaining of methods. + */ + @SafeVarargs + public final ExpressionConfiguration withAdditionalFunctions( + Map.Entry... functions) { + Arrays.stream(functions) + .forEach(entry -> functionDictionary.addFunction(entry.getKey(), entry.getValue())); + return this; + } + + private static Map getStandardConstants() { + + Map constants = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + constants.put("TRUE", new EvaluationValue(true)); + constants.put("FALSE", new EvaluationValue(false)); + constants.put( + "PI", + new EvaluationValue( + new BigDecimal( + "3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679"))); + constants.put( + "E", + new EvaluationValue( + new BigDecimal( + "2.71828182845904523536028747135266249775724709369995957496696762772407663"))); + + return constants; + } +} diff --git a/src/main/java/com/ezylang/evalex/config/FunctionDictionaryIfc.java b/src/main/java/com/ezylang/evalex/config/FunctionDictionaryIfc.java new file mode 100644 index 0000000..4fcddfa --- /dev/null +++ b/src/main/java/com/ezylang/evalex/config/FunctionDictionaryIfc.java @@ -0,0 +1,52 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.config; + +import com.ezylang.evalex.functions.FunctionIfc; + +/** + * A function dictionary holds all the functions, that can be used in an expression.
+ * The default implementation is the {@link MapBasedFunctionDictionary}. + */ +public interface FunctionDictionaryIfc { + + /** + * Allows to add a function to the dictionary. Implementation is optional, if you have a fixed set + * of functions, this method can throw an exception. + * + * @param functionName The function name. + * @param function The function implementation. + */ + void addFunction(String functionName, FunctionIfc function); + + /** + * Check if the dictionary has a function with that name. + * + * @param functionName The function name to look for. + * @return true if a function was found or false if not. + */ + default boolean hasFunction(String functionName) { + return getFunction(functionName) != null; + } + + /** + * Get the function definition for a function name. + * + * @param functionName The name of the function. + * @return The function definition or null if no function was found. + */ + FunctionIfc getFunction(String functionName); +} diff --git a/src/main/java/com/ezylang/evalex/config/MapBasedFunctionDictionary.java b/src/main/java/com/ezylang/evalex/config/MapBasedFunctionDictionary.java new file mode 100644 index 0000000..934de4f --- /dev/null +++ b/src/main/java/com/ezylang/evalex/config/MapBasedFunctionDictionary.java @@ -0,0 +1,55 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.config; + +import static java.util.Arrays.stream; + +import com.ezylang.evalex.functions.FunctionIfc; +import java.util.Map; +import java.util.TreeMap; + +/** + * A default case-insensitive implementation of the function dictionary that uses a local + * Map.Entry<String, FunctionIfc> for storage. + */ +public class MapBasedFunctionDictionary implements FunctionDictionaryIfc { + + private final Map functions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + /** + * Creates a new function dictionary with the specified list of functions. + * + * @param functions variable number of arguments that specify the function names and definitions + * that will initially be added. + * @return A newly created function dictionary with the specified functions. + */ + @SuppressWarnings({"unchecked", "varargs"}) + public static FunctionDictionaryIfc ofFunctions(Map.Entry... functions) { + FunctionDictionaryIfc dictionary = new MapBasedFunctionDictionary(); + stream(functions).forEach(entry -> dictionary.addFunction(entry.getKey(), entry.getValue())); + return dictionary; + } + + @Override + public FunctionIfc getFunction(String functionName) { + return functions.get(functionName); + } + + @Override + public void addFunction(String functionName, FunctionIfc function) { + functions.put(functionName, function); + } +} diff --git a/src/main/java/com/ezylang/evalex/config/MapBasedOperatorDictionary.java b/src/main/java/com/ezylang/evalex/config/MapBasedOperatorDictionary.java new file mode 100644 index 0000000..05e0ea2 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/config/MapBasedOperatorDictionary.java @@ -0,0 +1,75 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.config; + +import static java.util.Arrays.stream; + +import com.ezylang.evalex.operators.OperatorIfc; +import java.util.Map; +import java.util.TreeMap; + +/** + * A default case-insensitive implementation of the operator dictionary that uses a local + * Map.Entry<String,OperatorIfc> for storage. + */ +public class MapBasedOperatorDictionary implements OperatorDictionaryIfc { + + final Map prefixOperators = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + final Map postfixOperators = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + final Map infixOperators = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + /** + * Creates a new operator dictionary with the specified list of operators. + * + * @param operators variable number of arguments that specify the operator names and definitions + * that will initially be added. + * @return A newly created operator dictionary with the specified operators. + */ + @SuppressWarnings({"unchecked", "varargs"}) + public static OperatorDictionaryIfc ofOperators(Map.Entry... operators) { + OperatorDictionaryIfc dictionary = new MapBasedOperatorDictionary(); + stream(operators).forEach(entry -> dictionary.addOperator(entry.getKey(), entry.getValue())); + return dictionary; + } + + @Override + public void addOperator(String operatorString, OperatorIfc operator) { + if (operator.isPrefix()) { + prefixOperators.put(operatorString, operator); + } else if (operator.isPostfix()) { + postfixOperators.put(operatorString, operator); + } else { + infixOperators.put(operatorString, operator); + } + } + + @Override + public OperatorIfc getPrefixOperator(String operatorString) { + return prefixOperators.get(operatorString); + } + + @Override + public OperatorIfc getPostfixOperator(String operatorString) { + return postfixOperators.get(operatorString); + } + + @Override + public OperatorIfc getInfixOperator(String operatorString) { + return infixOperators.get(operatorString); + } +} diff --git a/src/main/java/com/ezylang/evalex/config/OperatorDictionaryIfc.java b/src/main/java/com/ezylang/evalex/config/OperatorDictionaryIfc.java new file mode 100644 index 0000000..61f0538 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/config/OperatorDictionaryIfc.java @@ -0,0 +1,88 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.config; + +import com.ezylang.evalex.operators.OperatorIfc; + +/** + * An operator dictionary holds all the operators, that can be used in an expression.
+ * The default implementation is the {@link MapBasedOperatorDictionary}. + */ +public interface OperatorDictionaryIfc { + + /** + * Allows to add an operator to the dictionary. Implementation is optional, if you have a fixed + * set of operators, this method can throw an exception. + * + * @param operatorString The operator name. + * @param operator The operator implementation. + */ + void addOperator(String operatorString, OperatorIfc operator); + + /** + * Check if the dictionary has a prefix operator with that name. + * + * @param operatorString The operator name to look for. + * @return true if an operator was found or false if not. + */ + default boolean hasPrefixOperator(String operatorString) { + return getPrefixOperator(operatorString) != null; + } + + /** + * Check if the dictionary has a postfix operator with that name. + * + * @param operatorString The operator name to look for. + * @return true if an operator was found or false if not. + */ + default boolean hasPostfixOperator(String operatorString) { + return getPostfixOperator(operatorString) != null; + } + + /** + * Check if the dictionary has an infix operator with that name. + * + * @param operatorString The operator name to look for. + * @return true if an operator was found or false if not. + */ + default boolean hasInfixOperator(String operatorString) { + return getInfixOperator(operatorString) != null; + } + + /** + * Get the operator definition for a prefix operator name. + * + * @param operatorString The name of the operator. + * @return The operator definition or null if no operator was found. + */ + OperatorIfc getPrefixOperator(String operatorString); + + /** + * Get the operator definition for a postfix operator name. + * + * @param operatorString The name of the operator. + * @return The operator definition or null if no operator was found. + */ + OperatorIfc getPostfixOperator(String operatorString); + + /** + * Get the operator definition for an infix operator name. + * + * @param operatorString The name of the operator. + * @return The operator definition or null if no operator was found. + */ + OperatorIfc getInfixOperator(String operatorString); +} diff --git a/src/main/java/com/ezylang/evalex/data/DataAccessorIfc.java b/src/main/java/com/ezylang/evalex/data/DataAccessorIfc.java new file mode 100644 index 0000000..cbba724 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/DataAccessorIfc.java @@ -0,0 +1,40 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data; + +/** + * A data accessor is responsible for accessing data, e.g. variable and constant values during an + * expression evaluation. The default implementation for setting and reading local data is the + * {@link MapBasedDataAccessor}. + */ +public interface DataAccessorIfc { + + /** + * Retrieves a data value. + * + * @param variable The variable name, e.g. a variable or constant name. + * @return The data value, or null if not found. + */ + EvaluationValue getData(String variable); + + /** + * Sets a data value. + * + * @param variable The variable name, e.g. a variable or constant name. + * @param value The value to set. + */ + void setData(String variable, EvaluationValue value); +} diff --git a/src/main/java/com/ezylang/evalex/data/EvaluationValue.java b/src/main/java/com/ezylang/evalex/data/EvaluationValue.java new file mode 100644 index 0000000..b59a3a2 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/EvaluationValue.java @@ -0,0 +1,363 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data; + +import com.ezylang.evalex.parser.ASTNode; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.MathContext; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import lombok.Value; + +/** + * The representation of the final or intermediate evaluation result value. The representation + * consists of a data type and data value. Depending on the type, the value will be stored in a + * corresponding object type. + */ +@Value +public class EvaluationValue implements Comparable { + + /** The supported data types. */ + public enum DataType { + /** A string of characters, stored as {@link String}. */ + STRING, + /** Any number, stored as {@link BigDecimal}. */ + NUMBER, + /** A boolean, stored as {@link Boolean}. */ + BOOLEAN, + /** A list evaluation values. Stored as {@link java.util.List}. */ + ARRAY, + /** + * A structure with pairs of name/value members. Name is a string and the value is a {@link + * EvaluationValue}. Stored as a {@link java.util.Map}. + */ + STRUCTURE, + /** + * Used for lazy parameter evaluation, stored as an {@link ASTNode}, which can be evaluated on + * demand. + */ + EXPRESSION_NODE + } + + Object value; + + DataType dataType; + + /** + * Creates a new evaluation value by taking a good guess on the provided Java class and converting + * it to one of the supported types. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Input typeStorage Type
BigDecimalBigDecimal
Long, longBigDecimal
Integer, intBigDecimal
Short, shortBigDecimal
Byte, byteBigDecimal
Double, doubleBigDecimal *
Float, floatBigDecimal *
CharSequence , StringString
Boolean, booleanBoolean
ASTNodeASTNode
List<?>List<EvaluationValue> - each entry will be converted
Map<?,?>Map<String><EvaluationValue> - each entry will be converted.
+ * + * * Be careful with conversion problems when using float or double, which are fractional + * numbers. A (float)0.1 is e.g. converted to 0.10000000149011612 + * + * @param value One of the supported data types. + * @throws IllegalArgumentException if the data type can't be mapped. + */ + public EvaluationValue(Object value) { + BigDecimal number = convertToBigDecimal(value); + if (number != null) { + this.dataType = DataType.NUMBER; + this.value = number; + } else if (value instanceof CharSequence) { + this.dataType = DataType.STRING; + this.value = ((CharSequence) value).toString(); + } else if (value instanceof Character) { + this.dataType = DataType.STRING; + this.value = ((Character) value).toString(); + } else if (value instanceof Boolean) { + this.dataType = DataType.BOOLEAN; + this.value = value; + } else if (value instanceof ASTNode) { + this.dataType = DataType.EXPRESSION_NODE; + this.value = value; + } else if (value instanceof List) { + this.dataType = DataType.ARRAY; + this.value = convertToList((List) value); + } else if (value instanceof Map) { + this.dataType = DataType.STRUCTURE; + this.value = convertMapStructure((Map) value); + } else { + throw new IllegalArgumentException( + "Unsupported data type '" + value.getClass().getName() + "'"); + } + } + + public EvaluationValue(double value, MathContext mathContext) { + this.dataType = DataType.NUMBER; + this.value = new BigDecimal(Double.toString(value), mathContext); + } + + /** + * Converts a {@link Map} of objects to a {@link Map} of {@link EvaluationValue} values. + * + * @return A {@link Map} of {@link EvaluationValue} values. + */ + private Map convertMapStructure(Map value) { + Map structure = new HashMap<>(); + for (Entry entry : value.entrySet()) { + String name = entry.getKey().toString(); + structure.put(name, new EvaluationValue(entry.getValue())); + } + return structure; + } + + /** + * Converts a {@link List} of objects to a {@link List} of {@link EvaluationValue} values. + * + * @return A {@link List} of {@link EvaluationValue} values. + */ + private List convertToList(List value) { + List array = new ArrayList<>(); + value.forEach(element -> array.add(new EvaluationValue(element))); + return array; + } + + /** + * Check and convert, if an {@link Object} can be converted to a {@link BigDecimal} value. + * + * @return A {@link BigDecimal} value of the object, or null if it can't be + * converted. + */ + private BigDecimal convertToBigDecimal(Object value) { + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } else if (value instanceof Double) { + return BigDecimal.valueOf((double) value); + } else if (value instanceof Float) { + return BigDecimal.valueOf((float) value); + } else if (value instanceof Integer) { + return BigDecimal.valueOf((int) value); + } else if (value instanceof Long) { + return BigDecimal.valueOf((long) value); + } else if (value instanceof Short) { + return BigDecimal.valueOf((short) value); + } else if (value instanceof Byte) { + return BigDecimal.valueOf((byte) value); + } else { + return null; + } + } + + /** + * Checks if the value is of type {@link DataType#NUMBER}. + * + * @return true or false. + */ + public boolean isNumberValue() { + return getDataType() == DataType.NUMBER; + } + + /** + * Checks if the value is of type {@link DataType#STRING}. + * + * @return true or false. + */ + public boolean isStringValue() { + return getDataType() == DataType.STRING; + } + + /** + * Checks if the value is of type {@link DataType#BOOLEAN}. + * + * @return true or false. + */ + public boolean isBooleanValue() { + return getDataType() == DataType.BOOLEAN; + } + + /** + * Checks if the value is of type {@link DataType#ARRAY}. + * + * @return true or false. + */ + public boolean isArrayValue() { + return getDataType() == DataType.ARRAY; + } + + /** + * Checks if the value is of type {@link DataType#STRUCTURE}. + * + * @return true or false. + */ + public boolean isStructureValue() { + return getDataType() == DataType.STRUCTURE; + } + + /** + * Checks if the value is of type {@link DataType#EXPRESSION_NODE}. + * + * @return true or false. + */ + public boolean isExpressionNode() { + return getDataType() == DataType.EXPRESSION_NODE; + } + + /** + * Creates a {@link DataType#NUMBER} value from a {@link String}. + * + * @param value The {@link String} value. + * @param mathContext The math context to use for creation of the {@link BigDecimal} storage. + */ + public static EvaluationValue numberOfString(String value, MathContext mathContext) { + if (value.startsWith("0x") || value.startsWith("0X")) { + BigInteger hexToInteger = new BigInteger(value.substring(2), 16); + return new EvaluationValue(new BigDecimal(hexToInteger, mathContext)); + } else { + return new EvaluationValue(new BigDecimal(value, mathContext)); + } + } + + /** + * Gets a {@link BigDecimal} representation of the value. If possible and needed, a conversion + * will be made. + * + *
    + *
  • Boolean true will return a {@link BigDecimal#ONE}, else {@link + * BigDecimal#ZERO}. + *
+ * + * @return The {@link BigDecimal} representation of the value, or {@link BigDecimal#ZERO} if + * conversion is not possible. + */ + public BigDecimal getNumberValue() { + switch (getDataType()) { + case NUMBER: + return (BigDecimal) value; + case BOOLEAN: + return (Boolean.TRUE.equals(value) ? BigDecimal.ONE : BigDecimal.ZERO); + case STRING: + return Boolean.parseBoolean((String) value) ? BigDecimal.ONE : BigDecimal.ZERO; + default: + return BigDecimal.ZERO; + } + } + + /** + * Gets a {@link String} representation of the value. If possible and needed, a conversion will be + * made. + * + *
    + *
  • Number values will be returned as {@link BigDecimal#toPlainString()}. + *
  • The {@link Object#toString()} will be used in all other cases. + *
+ * + * @return The {@link String} representation of the value. + */ + public String getStringValue() { + if (getDataType() == DataType.NUMBER) { + return ((BigDecimal) value).toPlainString(); + } + return value.toString(); + } + + /** + * Gets a {@link Boolean} representation of the value. If possible and needed, a conversion will + * be made. + * + *
    + *
  • Any non-zero number value will return true. + *
  • Any string with the value "true" (case ignored) will return true. + *
+ * + * @return The {@link Boolean} representation of the value. + */ + public Boolean getBooleanValue() { + switch (getDataType()) { + case NUMBER: + return !value.equals(BigDecimal.ZERO); + case BOOLEAN: + return (Boolean) value; + case STRING: + return Boolean.parseBoolean((String) value); + default: + return false; + } + } + + /** + * Gets a {@link List} representation of the value. + * + * @return The {@link List} representation of the value or an empty list, if no + * conversion is possible. + */ + @SuppressWarnings("unchecked") + public List getArrayValue() { + if (isArrayValue()) { + return (List) value; + } else { + return Collections.emptyList(); + } + } + + /** + * Gets a {@link Map} representation of the value. + * + * @return The {@link Map} representation of the value or an empty list, if no conversion is + * possible. + */ + @SuppressWarnings("unchecked") + public Map getStructureValue() { + if (isStructureValue()) { + return (Map) value; + } else { + return Collections.emptyMap(); + } + } + + /** + * Gets the expression node, if this value is of type {@link DataType#EXPRESSION_NODE}. + * + * @return The expression node, or null for any other data type. + */ + public ASTNode getExpressionNode() { + return isExpressionNode() ? ((ASTNode) getValue()) : null; + } + + @Override + public int compareTo(EvaluationValue toCompare) { + switch (getDataType()) { + case NUMBER: + return getNumberValue().compareTo(toCompare.getNumberValue()); + case BOOLEAN: + return getBooleanValue().compareTo(toCompare.getBooleanValue()); + default: + return getStringValue().compareTo(toCompare.getStringValue()); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/data/MapBasedDataAccessor.java b/src/main/java/com/ezylang/evalex/data/MapBasedDataAccessor.java new file mode 100644 index 0000000..33015d3 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/MapBasedDataAccessor.java @@ -0,0 +1,39 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data; + +import java.util.Map; +import java.util.TreeMap; + +/** + * A default case-insensitive implementation of the data accessor that uses a local + * Map.Entry<String, EvaluationValue> for storage. + */ +public class MapBasedDataAccessor implements DataAccessorIfc { + + private final Map variables = + new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + @Override + public EvaluationValue getData(String variable) { + return variables.get(variable); + } + + @Override + public void setData(String variable, EvaluationValue value) { + variables.put(variable, value); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/AbstractFunction.java b/src/main/java/com/ezylang/evalex/functions/AbstractFunction.java new file mode 100644 index 0000000..a6b96d0 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/AbstractFunction.java @@ -0,0 +1,99 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.Token; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * Abstract implementation of the {@link FunctionIfc}, used as base class for function + * implementations. + */ +public abstract class AbstractFunction implements FunctionIfc { + + private final List functionParameterDefinitions = new ArrayList<>(); + + private final boolean hasVarArgs; + + /** + * Creates a new function and uses the {@link FunctionParameter} annotations to create the + * parameter definitions. + */ + protected AbstractFunction() { + FunctionParameter[] parameterAnnotations = + getClass().getAnnotationsByType(FunctionParameter.class); + + boolean varArgParameterFound = false; + + for (FunctionParameter parameter : parameterAnnotations) { + if (varArgParameterFound) { + throw new IllegalArgumentException( + "Only last parameter may be defined as variable argument"); + } + if (parameter.isVarArg()) { + varArgParameterFound = true; + } + functionParameterDefinitions.add( + FunctionParameterDefinition.builder() + .name(parameter.name()) + .isVarArg(parameter.isVarArg()) + .isLazy(parameter.isLazy()) + .nonZero(parameter.nonZero()) + .nonNegative(parameter.nonNegative()) + .build()); + } + + hasVarArgs = varArgParameterFound; + } + + @Override + public void validatePreEvaluation(Token token, EvaluationValue... parameterValues) + throws EvaluationException { + + for (int i = 0; i < parameterValues.length; i++) { + FunctionParameterDefinition definition = getParameterDefinitionForParameter(i); + if (definition.isNonZero() && parameterValues[i].getNumberValue().equals(BigDecimal.ZERO)) { + throw new EvaluationException(token, "Parameter must not be zero"); + } + if (definition.isNonNegative() && parameterValues[i].getNumberValue().signum() < 0) { + throw new EvaluationException(token, "Parameter must not be negative"); + } + } + } + + @Override + public List getFunctionParameterDefinitions() { + return functionParameterDefinitions; + } + + @Override + public boolean hasVarArgs() { + return hasVarArgs; + } + + private FunctionParameterDefinition getParameterDefinitionForParameter(int index) { + + if (hasVarArgs && index >= functionParameterDefinitions.size()) { + index = functionParameterDefinitions.size() - 1; + } + + return functionParameterDefinitions.get(index); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/FunctionIfc.java b/src/main/java/com/ezylang/evalex/functions/FunctionIfc.java new file mode 100644 index 0000000..beb6397 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/FunctionIfc.java @@ -0,0 +1,82 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.Token; +import java.util.List; + +/** + * Interface that is required for all functions in a function dictionary for evaluation of + * expressions. + */ +public interface FunctionIfc { + + /** + * Returns the list of parameter definitions. Is never empty or null. + * + * @return The parameter definition list. + */ + List getFunctionParameterDefinitions(); + + /** + * Performs the function logic and returns an evaluation result. + * + * @param expression The expression, where this function is executed. Can be used to access the + * expression configuration. + * @param functionToken The function token from the parsed expression. + * @param parameterValues The parameter values. + * @return The evaluation result in form of a {@link EvaluationValue}. + * @throws EvaluationException In case there were problems during evaluation. + */ + EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException; + + /** + * Validates the evaluation parameters, called before the actual evaluation. + * + * @param token The function token. + * @param parameterValues The parameter values + * @throws EvaluationException in case of any validation error + */ + void validatePreEvaluation(Token token, EvaluationValue... parameterValues) + throws EvaluationException; + + /** + * Checks whether the function has a variable number of arguments parameter. + * + * @return true or false: + */ + boolean hasVarArgs(); + + /** + * Checks if the parameter is a lazy parameter. + * + * @param parameterIndex The parameter index, starts at 0 for the first parameter. If the index is + * bigger than the list of parameter definitions, the last parameter definition will be + * checked. + * @return true if the specified parameter is defined as lazy. + */ + default boolean isParameterLazy(int parameterIndex) { + if (parameterIndex >= getFunctionParameterDefinitions().size()) { + parameterIndex = getFunctionParameterDefinitions().size() - 1; + } + return getFunctionParameterDefinitions().get(parameterIndex).isLazy(); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/FunctionParameter.java b/src/main/java/com/ezylang/evalex/functions/FunctionParameter.java new file mode 100644 index 0000000..2a5c429 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/FunctionParameter.java @@ -0,0 +1,46 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Annotation to define a function parameter. */ +@Documented +@Target(ElementType.TYPE) +@Repeatable(FunctionParameters.class) +@Retention(RetentionPolicy.RUNTIME) +public @interface FunctionParameter { + + /** The parameter name. */ + String name(); + + /** If the parameter is lazily evaluated. Defaults to false. */ + boolean isLazy() default false; + + /** If the parameter is a variable arg type (repeatable). Defaults to false. */ + boolean isVarArg() default false; + + /** If the parameter does not allow zero values. */ + boolean nonZero() default false; + + /** If the parameter does not allow negative values. */ + boolean nonNegative() default false; +} diff --git a/src/main/java/com/ezylang/evalex/functions/FunctionParameterDefinition.java b/src/main/java/com/ezylang/evalex/functions/FunctionParameterDefinition.java new file mode 100644 index 0000000..042b794 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/FunctionParameterDefinition.java @@ -0,0 +1,49 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions; + +import lombok.Builder; +import lombok.Value; + +/** Definition of a function parameter. */ +@Value +@Builder +public class FunctionParameterDefinition { + + /** Name of the parameter, useful for error messages etc. */ + String name; + + /** + * Whether this parameter is a variable argument parameter (can be repeated). + * + * @see com.ezylang.evalex.functions.basic.MinFunction for an example. + */ + boolean isVarArg; + + /** + * Set to true, the parameter will not be evaluated in advance, but the corresponding {@link + * com.ezylang.evalex.parser.ASTNode} will be passed as a parameter value. + * + * @see com.ezylang.evalex.functions.basic.IfFunction for an example. + */ + boolean isLazy; + + /** If the parameter does not allow zero values. */ + boolean nonZero; + + /** If the parameter does not allow negative values. */ + boolean nonNegative; +} diff --git a/src/main/java/com/ezylang/evalex/functions/FunctionParameters.java b/src/main/java/com/ezylang/evalex/functions/FunctionParameters.java new file mode 100644 index 0000000..6edc526 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/FunctionParameters.java @@ -0,0 +1,30 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Collator for repeatable {@link FunctionParameter} annotations. */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface FunctionParameters { + FunctionParameter[] value(); +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/AbsFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/AbsFunction.java new file mode 100644 index 0000000..18302ce --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/AbsFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Absolute (non-negative) value. */ +@FunctionParameter(name = "value") +public class AbsFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return new EvaluationValue( + parameterValues[0].getNumberValue().abs(expression.getConfiguration().getMathContext())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/CeilingFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/CeilingFunction.java new file mode 100644 index 0000000..02db39c --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/CeilingFunction.java @@ -0,0 +1,36 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; +import java.math.RoundingMode; + +/** Rounds the given value an integer using the rounding mode {@link RoundingMode#CEILING} */ +@FunctionParameter(name = "value") +public class CeilingFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + EvaluationValue value = parameterValues[0]; + + return new EvaluationValue(value.getNumberValue().setScale(0, RoundingMode.CEILING)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/FactFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/FactFunction.java new file mode 100644 index 0000000..aff60a5 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/FactFunction.java @@ -0,0 +1,42 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; +import java.math.BigDecimal; + +/** Factorial function, calculates the factorial of a base value. */ +@FunctionParameter(name = "base") +public class FactFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + int number = parameterValues[0].getNumberValue().intValue(); + BigDecimal factorial = BigDecimal.ONE; + for (int i = 1; i <= number; i++) { + factorial = + factorial.multiply( + new BigDecimal(i, expression.getConfiguration().getMathContext()), + expression.getConfiguration().getMathContext()); + } + return new EvaluationValue(factorial); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/FloorFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/FloorFunction.java new file mode 100644 index 0000000..edfde3e --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/FloorFunction.java @@ -0,0 +1,36 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; +import java.math.RoundingMode; + +/** Rounds the given value an integer using the rounding mode {@link RoundingMode#FLOOR} */ +@FunctionParameter(name = "value") +public class FloorFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + EvaluationValue value = parameterValues[0]; + + return new EvaluationValue(value.getNumberValue().setScale(0, RoundingMode.FLOOR)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/IfFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/IfFunction.java new file mode 100644 index 0000000..a77d1eb --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/IfFunction.java @@ -0,0 +1,45 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Conditional evaluation function. If parameter condition is true, the + * resultIfTrue value is returned, else the resultIfFalse value. + * resultIfTrue and resultIfFalse are only evaluated (lazily evaluated), + * after the condition was evaluated. + */ +@FunctionParameter(name = "condition") +@FunctionParameter(name = "resultIfTrue", isLazy = true) +@FunctionParameter(name = "resultIfFalse", isLazy = true) +public class IfFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + if (Boolean.TRUE.equals(parameterValues[0].getBooleanValue())) { + return expression.evaluateSubtree(parameterValues[1].getExpressionNode()); + } else { + return expression.evaluateSubtree(parameterValues[2].getExpressionNode()); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/Log10Function.java b/src/main/java/com/ezylang/evalex/functions/basic/Log10Function.java new file mode 100644 index 0000000..72d816a --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/Log10Function.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** The base 10 logarithm of a value */ +@FunctionParameter(name = "value", nonZero = true, nonNegative = true) +public class Log10Function extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + double d = parameterValues[0].getNumberValue().doubleValue(); + + return expression.convertDoubleValue(Math.log10(d)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/LogFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/LogFunction.java new file mode 100644 index 0000000..59ede51 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/LogFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** The natural logarithm (base e) of a value */ +@FunctionParameter(name = "value", nonZero = true, nonNegative = true) +public class LogFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + double d = parameterValues[0].getNumberValue().doubleValue(); + + return expression.convertDoubleValue(Math.log(d)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/MaxFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/MaxFunction.java new file mode 100644 index 0000000..3a87a6a --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/MaxFunction.java @@ -0,0 +1,39 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; +import java.math.BigDecimal; + +/** Returns the maximum value of all parameters. */ +@FunctionParameter(name = "value", isVarArg = true) +public class MaxFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + BigDecimal max = null; + for (EvaluationValue parameter : parameterValues) { + if (max == null || parameter.getNumberValue().compareTo(max) > 0) { + max = parameter.getNumberValue(); + } + } + return new EvaluationValue(max); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/MinFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/MinFunction.java new file mode 100644 index 0000000..bbbb41e --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/MinFunction.java @@ -0,0 +1,39 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; +import java.math.BigDecimal; + +/** Returns the minimum value of all parameters. */ +@FunctionParameter(name = "value", isVarArg = true) +public class MinFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + BigDecimal min = null; + for (EvaluationValue parameter : parameterValues) { + if (min == null || parameter.getNumberValue().compareTo(min) < 0) { + min = parameter.getNumberValue(); + } + } + return new EvaluationValue(min); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/NotFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/NotFunction.java new file mode 100644 index 0000000..29724c5 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/NotFunction.java @@ -0,0 +1,34 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Boolean negation. */ +@FunctionParameter(name = "value") +public class NotFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return new EvaluationValue(!parameterValues[0].getBooleanValue()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/RandomFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/RandomFunction.java new file mode 100644 index 0000000..e2962b1 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/RandomFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.parser.Token; +import java.security.SecureRandom; + +/** Random function produces a random value between 0 and 1. */ +public class RandomFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + SecureRandom secureRandom = new SecureRandom(); + + return expression.convertDoubleValue(secureRandom.nextDouble()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/RoundFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/RoundFunction.java new file mode 100644 index 0000000..1d2c0e9 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/RoundFunction.java @@ -0,0 +1,45 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Rounds the given value to the specified scale, using the {@link java.math.MathContext} of the + * expression configuration. + */ +@FunctionParameter(name = "value") +@FunctionParameter(name = "scale") +public class RoundFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + EvaluationValue value = parameterValues[0]; + EvaluationValue precision = parameterValues[1]; + + return new EvaluationValue( + value + .getNumberValue() + .setScale( + precision.getNumberValue().intValue(), + expression.getConfiguration().getMathContext().getRoundingMode())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/SqrtFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/SqrtFunction.java new file mode 100644 index 0000000..ef91138 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/SqrtFunction.java @@ -0,0 +1,61 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.MathContext; + +/** Square root function, uses the standard {@link BigDecimal#sqrt(MathContext)} implementation. */ +@FunctionParameter(name = "value", nonNegative = true) +public class SqrtFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* + * From The Java Programmers Guide To numerical Computing + * (Ronald Mak, 2003) + */ + BigDecimal x = parameterValues[0].getNumberValue(); + MathContext mathContext = expression.getConfiguration().getMathContext(); + + if (x.compareTo(BigDecimal.ZERO) == 0) { + return new EvaluationValue(BigDecimal.ZERO); + } + BigInteger n = x.movePointRight(mathContext.getPrecision() << 1).toBigInteger(); + + int bits = (n.bitLength() + 1) >> 1; + BigInteger ix = n.shiftRight(bits); + BigInteger ixPrev; + BigInteger test; + do { + ixPrev = ix; + ix = ix.add(n.divide(ix)).shiftRight(1); + // Give other threads a chance to work + Thread.yield(); + test = ix.subtract(ixPrev).abs(); + } while (test.compareTo(BigInteger.ZERO) != 0 && test.compareTo(BigInteger.ONE) != 0); + + return new EvaluationValue(new BigDecimal(ix, mathContext.getPrecision())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/SumFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/SumFunction.java new file mode 100644 index 0000000..9010c2d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/SumFunction.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; +import java.math.BigDecimal; + +/** Returns the sum value of all parameters. */ +@FunctionParameter(name = "value", isVarArg = true) +public class SumFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + BigDecimal sum = BigDecimal.ZERO; + for (EvaluationValue parameter : parameterValues) { + sum = sum.add(parameter.getNumberValue(), expression.getConfiguration().getMathContext()); + } + return new EvaluationValue(sum); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringContains.java b/src/main/java/com/ezylang/evalex/functions/string/StringContains.java new file mode 100644 index 0000000..886ebf0 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringContains.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns true, if the string contains the substring (case-insensitive). */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "substring") +public class StringContains extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String string = parameterValues[0].getStringValue(); + String substring = parameterValues[1].getStringValue(); + return new EvaluationValue(string.toUpperCase().contains(substring.toUpperCase())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringLowerFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringLowerFunction.java new file mode 100644 index 0000000..a3c7c38 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringLowerFunction.java @@ -0,0 +1,32 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Converts the given value to lower case. */ +@FunctionParameter(name = "value") +public class StringLowerFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + return new EvaluationValue(parameterValues[0].getStringValue().toLowerCase()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringUpperFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringUpperFunction.java new file mode 100644 index 0000000..efc223d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringUpperFunction.java @@ -0,0 +1,32 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Converts the given value to upper case. */ +@FunctionParameter(name = "value") +public class StringUpperFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + return new EvaluationValue(parameterValues[0].getStringValue().toUpperCase()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosFunction.java new file mode 100644 index 0000000..05926f5 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosFunction.java @@ -0,0 +1,34 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the arc-cosine (in degrees). */ +@FunctionParameter(name = "value") +public class AcosFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.toDegrees(Math.acos(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosHFunction.java new file mode 100644 index 0000000..c9600c5 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosHFunction.java @@ -0,0 +1,40 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the hyperbolic arc-cosine (in degrees). */ +@FunctionParameter(name = "value") +public class AcosHFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + /* Formula: acosh(x) = ln(x + sqrt(x^2 - 1)) */ + double value = parameterValues[0].getNumberValue().doubleValue(); + if (Double.compare(value, 1) < 0) { + throw new EvaluationException(functionToken, "Value must be greater or equal to one"); + } + return expression.convertDoubleValue(Math.log(value + (Math.sqrt(Math.pow(value, 2) - 1)))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosRFunction.java new file mode 100644 index 0000000..0951227 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosRFunction.java @@ -0,0 +1,34 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the arc-cosine (in radians). */ +@FunctionParameter(name = "cosine") +public class AcosRFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.acos(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotFunction.java new file mode 100644 index 0000000..58847ef --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the arc-co-tangent (in degrees). */ +@FunctionParameter(name = "value", nonZero = true) +public class AcotFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: acot(x) = atan(1/x) */ + return expression.convertDoubleValue( + Math.toDegrees(Math.atan(1 / parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotRFunction.java new file mode 100644 index 0000000..7c76d2e --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotRFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the arc-co-tangent (in radians). */ +@FunctionParameter(name = "value", nonZero = true) +public class AcotRFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: acot(x) = atan(1/x) */ + return expression.convertDoubleValue( + Math.atan(1 / parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinFunction.java new file mode 100644 index 0000000..ab2b988 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinFunction.java @@ -0,0 +1,34 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the arc-sine (in degrees). */ +@FunctionParameter(name = "value") +public class AsinFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.toDegrees(Math.asin(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinHFunction.java new file mode 100644 index 0000000..bf61212 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinHFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the hyperbolic arc-sine (in degrees). */ +@FunctionParameter(name = "value") +public class AsinHFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: asinh(x) = ln(x + sqrt(x^2 + 1)) */ + double value = parameterValues[0].getNumberValue().doubleValue(); + return expression.convertDoubleValue(Math.log(value + (Math.sqrt(Math.pow(value, 2) + 1)))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinRFunction.java new file mode 100644 index 0000000..fb69fde --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinRFunction.java @@ -0,0 +1,34 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the arc-sine (in radians). */ +@FunctionParameter(name = "value") +public class AsinRFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.asin(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2Function.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2Function.java new file mode 100644 index 0000000..30af4b8 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2Function.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the angle of atan2 (in degrees). */ +@FunctionParameter(name = "y") +@FunctionParameter(name = "x") +public class Atan2Function extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.toDegrees( + Math.atan2( + parameterValues[0].getNumberValue().doubleValue(), + parameterValues[1].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2RFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2RFunction.java new file mode 100644 index 0000000..265cd4e --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2RFunction.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the angle of atan2 (in radians). */ +@FunctionParameter(name = "y") +@FunctionParameter(name = "x") +public class Atan2RFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.atan2( + parameterValues[0].getNumberValue().doubleValue(), + parameterValues[1].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanFunction.java new file mode 100644 index 0000000..4fcf839 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanFunction.java @@ -0,0 +1,34 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the arc-tangent (in degrees). */ +@FunctionParameter(name = "value") +public class AtanFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.toDegrees(Math.atan(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanHFunction.java new file mode 100644 index 0000000..a9dd78b --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanHFunction.java @@ -0,0 +1,40 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the hyperbolic arc-sine (in degrees). */ +@FunctionParameter(name = "value") +public class AtanHFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + /* Formula: atanh(x) = 0.5*ln((1 + x)/(1 - x)) */ + double value = parameterValues[0].getNumberValue().doubleValue(); + if (Math.abs(value) >= 1) { + throw new EvaluationException(functionToken, "Absolute value must be less than 1"); + } + return expression.convertDoubleValue(0.5 * Math.log((1 + value) / (1 - value))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanRFunction.java new file mode 100644 index 0000000..d40670d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanRFunction.java @@ -0,0 +1,34 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the arc-tangent (in radians). */ +@FunctionParameter(name = "value") +public class AtanRFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.atan(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CosFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CosFunction.java new file mode 100644 index 0000000..279dbac --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CosFunction.java @@ -0,0 +1,34 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the trigonometric cosine of an angle (in degrees). */ +@FunctionParameter(name = "value") +public class CosFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.cos(Math.toRadians(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CosHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CosHFunction.java new file mode 100644 index 0000000..2cbf6f7 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CosHFunction.java @@ -0,0 +1,34 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the hyperbolic cosine of a value. */ +@FunctionParameter(name = "value") +public class CosHFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.cosh(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CosRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CosRFunction.java new file mode 100644 index 0000000..63f82ea --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CosRFunction.java @@ -0,0 +1,34 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the trigonometric cosine of an angle (in radians). */ +@FunctionParameter(name = "value") +public class CosRFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.cos(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CotFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CotFunction.java new file mode 100644 index 0000000..a61d887 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CotFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the co-tangent of an angle (in degrees). */ +@FunctionParameter(name = "value", nonZero = true) +public class CotFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: cot(x) = cos(x) / sin(x) = 1 / tan(x) */ + return expression.convertDoubleValue( + 1 / Math.tan(Math.toRadians(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CotHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CotHFunction.java new file mode 100644 index 0000000..680abf7 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CotHFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the hyperbolic co-tangent of a value. */ +@FunctionParameter(name = "value", nonZero = true) +public class CotHFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: coth(x) = 1 / tanh(x) */ + return expression.convertDoubleValue( + 1 / Math.tanh(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CotRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CotRFunction.java new file mode 100644 index 0000000..97404cc --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CotRFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the trigonometric co-tangent of an angle (in radians). */ +@FunctionParameter(name = "value", nonZero = true) +public class CotRFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: cot(x) = cos(x) / sin(x) = 1 / tan(x) */ + return expression.convertDoubleValue( + 1 / Math.tan(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CscFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CscFunction.java new file mode 100644 index 0000000..d1183d2 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CscFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the co-secant (in degrees). */ +@FunctionParameter(name = "value", nonZero = true) +public class CscFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: csc(x) = 1 / sin(x) */ + return expression.convertDoubleValue( + 1 / Math.sin(Math.toRadians(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CscHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CscHFunction.java new file mode 100644 index 0000000..7249c7f --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CscHFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the co-secant (in degrees). */ +@FunctionParameter(name = "value", nonZero = true) +public class CscHFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: csch(x) = 1 / sinh(x) */ + return expression.convertDoubleValue( + 1 / Math.sinh(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CscRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CscRFunction.java new file mode 100644 index 0000000..cbd348e --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CscRFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the co-secant (in radians). */ +@FunctionParameter(name = "value", nonZero = true) +public class CscRFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: csc(x) = 1 / sin(x) */ + return expression.convertDoubleValue( + 1 / Math.sin(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/DegFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/DegFunction.java new file mode 100644 index 0000000..3d748fb --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/DegFunction.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Converts an angle measured in radians to an approximately equivalent angle measured in degrees. + */ +@FunctionParameter(name = "radians") +public class DegFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + double rad = Math.toDegrees(parameterValues[0].getNumberValue().doubleValue()); + + return expression.convertDoubleValue(rad); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/RadFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/RadFunction.java new file mode 100644 index 0000000..dfbdc20 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/RadFunction.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Converts an angle measured in degrees to an approximately equivalent angle measured in radians. + */ +@FunctionParameter(name = "degrees") +public class RadFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + double deg = Math.toRadians(parameterValues[0].getNumberValue().doubleValue()); + + return expression.convertDoubleValue(deg); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/SecFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/SecFunction.java new file mode 100644 index 0000000..0547593 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/SecFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the secant (in degrees). */ +@FunctionParameter(name = "value", nonZero = true) +public class SecFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: sec(x) = 1 / cos(x) */ + return expression.convertDoubleValue( + 1 / Math.cos(Math.toRadians(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/SecHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/SecHFunction.java new file mode 100644 index 0000000..3a1a3eb --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/SecHFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the hyperbolic secant (in degrees). */ +@FunctionParameter(name = "value", nonZero = true) +public class SecHFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: sech(x) = 1 / cosh(x) */ + return expression.convertDoubleValue( + 1 / Math.cosh(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/SecRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/SecRFunction.java new file mode 100644 index 0000000..634c76d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/SecRFunction.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the secant (in radians). */ +@FunctionParameter(name = "value", nonZero = true) +public class SecRFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: sec(x) = 1 / cos(x) */ + return expression.convertDoubleValue( + 1 / Math.cos(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/SinFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/SinFunction.java new file mode 100644 index 0000000..0ab67fc --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/SinFunction.java @@ -0,0 +1,34 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the trigonometric sine of an angle (in degrees). */ +@FunctionParameter(name = "value") +public class SinFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.sin(Math.toRadians(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/SinHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/SinHFunction.java new file mode 100644 index 0000000..40f9582 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/SinHFunction.java @@ -0,0 +1,34 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the hyperbolic sine of a value. */ +@FunctionParameter(name = "value") +public class SinHFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.sinh(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/SinRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/SinRFunction.java new file mode 100644 index 0000000..7ce01ac --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/SinRFunction.java @@ -0,0 +1,34 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the trigonometric sine of an angle (in radians). */ +@FunctionParameter(name = "value") +public class SinRFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.sin(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/TanFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/TanFunction.java new file mode 100644 index 0000000..5d99051 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/TanFunction.java @@ -0,0 +1,34 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the trigonometric tangent of an angle (in degrees). */ +@FunctionParameter(name = "value") +public class TanFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.tan(Math.toRadians(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/TanHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/TanHFunction.java new file mode 100644 index 0000000..88c02c7 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/TanHFunction.java @@ -0,0 +1,34 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the hyperbolic tangent of a value. */ +@FunctionParameter(name = "value") +public class TanHFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.tanh(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/TanRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/TanRFunction.java new file mode 100644 index 0000000..2ba15cd --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/TanRFunction.java @@ -0,0 +1,34 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** Returns the trigonometric tangent of an angle (in radians). */ +@FunctionParameter(name = "value") +public class TanRFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.tan(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/AbstractOperator.java b/src/main/java/com/ezylang/evalex/operators/AbstractOperator.java new file mode 100644 index 0000000..056a9e9 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/AbstractOperator.java @@ -0,0 +1,84 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators; + +import static com.ezylang.evalex.operators.OperatorIfc.OperatorType.PREFIX_OPERATOR; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import lombok.Getter; + +/** + * Abstract implementation of the {@link OperatorIfc}, used as base class for operator + * implementations. + */ +public abstract class AbstractOperator implements OperatorIfc { + + @Getter private final int precedence; + + private final boolean leftAssociative; + + OperatorType type; + + /** + * Creates a new operator and uses the {@link InfixOperator} annotation to create the operator + * definition. + */ + protected AbstractOperator() { + InfixOperator infixAnnotation = getClass().getAnnotation(InfixOperator.class); + PrefixOperator prefixAnnotation = getClass().getAnnotation(PrefixOperator.class); + PostfixOperator postfixAnnotation = getClass().getAnnotation(PostfixOperator.class); + if (infixAnnotation != null) { + this.type = OperatorType.INFIX_OPERATOR; + this.precedence = infixAnnotation.precedence(); + this.leftAssociative = infixAnnotation.leftAssociative(); + } else if (prefixAnnotation != null) { + this.type = PREFIX_OPERATOR; + this.precedence = prefixAnnotation.precedence(); + this.leftAssociative = prefixAnnotation.leftAssociative(); + } else if (postfixAnnotation != null) { + this.type = OperatorType.POSTFIX_OPERATOR; + this.precedence = postfixAnnotation.precedence(); + this.leftAssociative = postfixAnnotation.leftAssociative(); + } else { + throw new OperatorAnnotationNotFoundException(this.getClass().getName()); + } + } + + @Override + public int getPrecedence(ExpressionConfiguration configuration) { + return getPrecedence(); + } + + @Override + public boolean isLeftAssociative() { + return leftAssociative; + } + + @Override + public boolean isPrefix() { + return type == PREFIX_OPERATOR; + } + + @Override + public boolean isPostfix() { + return type == OperatorType.POSTFIX_OPERATOR; + } + + @Override + public boolean isInfix() { + return type == OperatorType.INFIX_OPERATOR; + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/InfixOperator.java b/src/main/java/com/ezylang/evalex/operators/InfixOperator.java new file mode 100644 index 0000000..7527b3a --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/InfixOperator.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** The infix operator annotation */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface InfixOperator { + + /** Operator precedence, usually one from the constants in {@link OperatorIfc}. */ + int precedence(); + + /** Operator associativity, defaults to true. */ + boolean leftAssociative() default true; +} diff --git a/src/main/java/com/ezylang/evalex/operators/OperatorAnnotationNotFoundException.java b/src/main/java/com/ezylang/evalex/operators/OperatorAnnotationNotFoundException.java new file mode 100644 index 0000000..d1a2aab --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/OperatorAnnotationNotFoundException.java @@ -0,0 +1,27 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators; + +/** + * Operator properties are defined through a class annotation, this exception is thrown if no + * annotation was found when creating the operator instance. + */ +public class OperatorAnnotationNotFoundException extends RuntimeException { + + public OperatorAnnotationNotFoundException(String className) { + super("Operator annotation for '" + className + "' not found"); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/OperatorIfc.java b/src/main/java/com/ezylang/evalex/operators/OperatorIfc.java new file mode 100644 index 0000000..84e8641 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/OperatorIfc.java @@ -0,0 +1,126 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.Token; + +/** + * Interface that is required for all operators in an operator dictionary for evaluation of + * expressions. There are three operator type: prefix, postfix and infix. Every operator has a + * precedence, which defines the order of operator evaluation. The associativity of an operator is a + * property that determines how operators of the same precedence are grouped in the absence of + * parentheses. + */ +public interface OperatorIfc { + + /** The operator type. */ + enum OperatorType { + /** Unary prefix operator, like -x */ + PREFIX_OPERATOR, + /** Unary postfix operator,like x! */ + POSTFIX_OPERATOR, + /** Binary infix operator, like x+y */ + INFIX_OPERATOR + } + + /** Or operator precedence: || */ + int OPERATOR_PRECEDENCE_OR = 2; + + /** And operator precedence: && */ + int OPERATOR_PRECEDENCE_AND = 4; + + /** Equality operators precedence: =, ==, !=, <> */ + int OPERATOR_PRECEDENCE_EQUALITY = 7; + + /** Comparative operators precedence: <, >, <=, >= */ + int OPERATOR_PRECEDENCE_COMPARISON = 10; + + /** Additive operators precedence: + and - */ + int OPERATOR_PRECEDENCE_ADDITIVE = 20; + + /** Multiplicative operators precedence: *, /, % */ + int OPERATOR_PRECEDENCE_MULTIPLICATIVE = 30; + + /** Power operator precedence: ^ */ + int OPERATOR_PRECEDENCE_POWER = 40; + + /** Unary operators precedence: + and - as prefix */ + int OPERATOR_PRECEDENCE_UNARY = 60; + + /** + * An optional higher power operator precedence, higher than the unary prefix, e.g. -2^2 equals to + * 4 or -4, depending on precedence configuration. + */ + int OPERATOR_PRECEDENCE_POWER_HIGHER = 80; + + /** + * @return The operator's precedence. + */ + int getPrecedence(); + + /** + * If operators with same precedence are evaluated from left to right. + * + * @return The associativity. + */ + boolean isLeftAssociative(); + + /** + * If it is a prefix operator. + * + * @return true if it is a prefix operator. + */ + boolean isPrefix(); + + /** + * If it is a postfix operator. + * + * @return true if it is a postfix operator. + */ + boolean isPostfix(); + + /** + * If it is an infix operator. + * + * @return true if it is an infix operator. + */ + boolean isInfix(); + + /** + * Called during parsing, can be implemented to return a customized precedence. + * + * @param configuration The expression configuration. + * @return The default precedence from the operator annotation, or a customized value. + */ + int getPrecedence(ExpressionConfiguration configuration); + + /** + * Performs the operator logic and returns an evaluation result. + * + * @param expression The expression, where this function is executed. Can be used to access the + * expression configuration. + * @param operatorToken The operator token from the parsed expression. + * @param operands The operands, one for prefix and postfix operators, two for infix operators. + * @return The evaluation result in form of a {@link EvaluationValue}. + * @throws EvaluationException In case there were problems during evaluation. + */ + EvaluationValue evaluate(Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException; +} diff --git a/src/main/java/com/ezylang/evalex/operators/PostfixOperator.java b/src/main/java/com/ezylang/evalex/operators/PostfixOperator.java new file mode 100644 index 0000000..d40ddb6 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/PostfixOperator.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_UNARY; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** The postfix operator annotation */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface PostfixOperator { + + /** Operator precedence, usually one from the constants in {@link OperatorIfc}. */ + int precedence() default OPERATOR_PRECEDENCE_UNARY; + + /** Operator associativity, defaults to true. */ + boolean leftAssociative() default true; +} diff --git a/src/main/java/com/ezylang/evalex/operators/PrefixOperator.java b/src/main/java/com/ezylang/evalex/operators/PrefixOperator.java new file mode 100644 index 0000000..f85a30f --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/PrefixOperator.java @@ -0,0 +1,37 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_UNARY; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** The prefix operator annotation */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface PrefixOperator { + + /** Operator precedence, usually one from the constants in {@link OperatorIfc}. */ + int precedence() default OPERATOR_PRECEDENCE_UNARY; + + /** Operator associativity, defaults to true. */ + boolean leftAssociative() default true; +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixDivisionOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixDivisionOperator.java new file mode 100644 index 0000000..68624c4 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixDivisionOperator.java @@ -0,0 +1,54 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_MULTIPLICATIVE; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; +import java.math.BigDecimal; + +/** Division of two numbers. */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_MULTIPLICATIVE) +public class InfixDivisionOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue leftOperand = operands[0]; + EvaluationValue rightOperand = operands[1]; + + if (leftOperand.isNumberValue() && rightOperand.isNumberValue()) { + + if (rightOperand.getNumberValue().equals(BigDecimal.ZERO)) { + throw new EvaluationException(operatorToken, "Division by zero"); + } + + return new EvaluationValue( + leftOperand + .getNumberValue() + .divide( + rightOperand.getNumberValue(), expression.getConfiguration().getMathContext())); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMinusOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMinusOperator.java new file mode 100644 index 0000000..248966b --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMinusOperator.java @@ -0,0 +1,48 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_ADDITIVE; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +/** Subtraction of two numbers. */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_ADDITIVE) +public class InfixMinusOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue leftOperand = operands[0]; + EvaluationValue rightOperand = operands[1]; + + if (leftOperand.isNumberValue() && rightOperand.isNumberValue()) { + return new EvaluationValue( + leftOperand + .getNumberValue() + .subtract( + rightOperand.getNumberValue(), expression.getConfiguration().getMathContext())); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixModuloOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixModuloOperator.java new file mode 100644 index 0000000..dcb9ff6 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixModuloOperator.java @@ -0,0 +1,54 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_MULTIPLICATIVE; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; +import java.math.BigDecimal; + +/** Remainder (modulo) of two numbers. */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_MULTIPLICATIVE) +public class InfixModuloOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue leftOperand = operands[0]; + EvaluationValue rightOperand = operands[1]; + + if (leftOperand.isNumberValue() && rightOperand.isNumberValue()) { + + if (rightOperand.getNumberValue().equals(BigDecimal.ZERO)) { + throw new EvaluationException(operatorToken, "Division by zero"); + } + + return new EvaluationValue( + leftOperand + .getNumberValue() + .remainder( + rightOperand.getNumberValue(), expression.getConfiguration().getMathContext())); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMultiplicationOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMultiplicationOperator.java new file mode 100644 index 0000000..5776c91 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMultiplicationOperator.java @@ -0,0 +1,48 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_MULTIPLICATIVE; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +/** Multiplication of two numbers. */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_MULTIPLICATIVE) +public class InfixMultiplicationOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue leftOperand = operands[0]; + EvaluationValue rightOperand = operands[1]; + + if (leftOperand.isNumberValue() && rightOperand.isNumberValue()) { + return new EvaluationValue( + leftOperand + .getNumberValue() + .multiply( + rightOperand.getNumberValue(), expression.getConfiguration().getMathContext())); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPlusOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPlusOperator.java new file mode 100644 index 0000000..fede27c --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPlusOperator.java @@ -0,0 +1,47 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_ADDITIVE; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +/** + * Addition of numbers and strings. If one operand is a string, a string concatenation is performed. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_ADDITIVE) +public class InfixPlusOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + EvaluationValue leftOperand = operands[0]; + EvaluationValue rightOperand = operands[1]; + + if (leftOperand.isNumberValue() && rightOperand.isNumberValue()) { + return new EvaluationValue( + leftOperand + .getNumberValue() + .add(rightOperand.getNumberValue(), expression.getConfiguration().getMathContext())); + } else { + return new EvaluationValue(leftOperand.getStringValue() + rightOperand.getStringValue()); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPowerOfOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPowerOfOperator.java new file mode 100644 index 0000000..f5d4f51 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPowerOfOperator.java @@ -0,0 +1,79 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_POWER; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; + +/** + * Power of operator, calculates the power of right operand of left operand. The precedence is read + * from the configuration during parsing. + * + * @see #getPrecedence(ExpressionConfiguration) + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_POWER, leftAssociative = false) +public class InfixPowerOfOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue leftOperand = operands[0]; + EvaluationValue rightOperand = operands[1]; + + if (leftOperand.isNumberValue() && rightOperand.isNumberValue()) { + /*- + * Thanks to Gene Marin: + * http://stackoverflow.com/questions/3579779/how-to-do-a-fractional-power-on-bigdecimal-in-java + */ + + MathContext mathContext = expression.getConfiguration().getMathContext(); + BigDecimal v1 = leftOperand.getNumberValue(); + BigDecimal v2 = rightOperand.getNumberValue(); + + int signOf2 = v2.signum(); + double dn1 = v1.doubleValue(); + v2 = v2.multiply(new BigDecimal(signOf2)); // n2 is now positive + BigDecimal remainderOf2 = v2.remainder(BigDecimal.ONE); + BigDecimal n2IntPart = v2.subtract(remainderOf2); + BigDecimal intPow = v1.pow(n2IntPart.intValueExact(), mathContext); + BigDecimal doublePow = BigDecimal.valueOf(Math.pow(dn1, remainderOf2.doubleValue())); + + BigDecimal result = intPow.multiply(doublePow, mathContext); + if (signOf2 == -1) { + result = BigDecimal.ONE.divide(result, mathContext.getPrecision(), RoundingMode.HALF_UP); + } + return new EvaluationValue(result); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } + + @Override + public int getPrecedence(ExpressionConfiguration configuration) { + return configuration.getPowerOfPrecedence(); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixMinusOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixMinusOperator.java new file mode 100644 index 0000000..c82a968 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixMinusOperator.java @@ -0,0 +1,42 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.PrefixOperator; +import com.ezylang.evalex.parser.Token; + +/** Unary prefix minus. */ +@PrefixOperator(leftAssociative = false) +public class PrefixMinusOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue operand = operands[0]; + + if (operand.isNumberValue()) { + return new EvaluationValue( + operand.getNumberValue().negate(expression.getConfiguration().getMathContext())); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixPlusOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixPlusOperator.java new file mode 100644 index 0000000..6b4436b --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixPlusOperator.java @@ -0,0 +1,42 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.PrefixOperator; +import com.ezylang.evalex.parser.Token; + +/** Unary prefix plus. */ +@PrefixOperator(leftAssociative = false) +public class PrefixPlusOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue operator = operands[0]; + + if (operator.isNumberValue()) { + return new EvaluationValue( + operator.getNumberValue().plus(expression.getConfiguration().getMathContext())); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixAndOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixAndOperator.java new file mode 100644 index 0000000..d3c96f0 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixAndOperator.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_AND; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +/** Boolean AND of two values. */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_AND) +public class InfixAndOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return new EvaluationValue(operands[0].getBooleanValue() && operands[1].getBooleanValue()); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperator.java new file mode 100644 index 0000000..b373c18 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperator.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_EQUALITY; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +/** Equality of two values. */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_EQUALITY) +public class InfixEqualsOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return new EvaluationValue(operands[0].equals(operands[1])); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperator.java new file mode 100644 index 0000000..d846842 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperator.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_COMPARISON; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +/** Greater or equals of two values. */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON) +public class InfixGreaterEqualsOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return new EvaluationValue(operands[0].compareTo(operands[1]) >= 0); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperator.java new file mode 100644 index 0000000..431d957 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperator.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_COMPARISON; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +/** Greater of two values. */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON) +public class InfixGreaterOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return new EvaluationValue(operands[0].compareTo(operands[1]) > 0); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperator.java new file mode 100644 index 0000000..5fde2af --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperator.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_COMPARISON; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +/** Less or equals of two values. */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON) +public class InfixLessEqualsOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return new EvaluationValue(operands[0].compareTo(operands[1]) <= 0); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixLessOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixLessOperator.java new file mode 100644 index 0000000..3234fc3 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixLessOperator.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_COMPARISON; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +/** Less of two values. */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON) +public class InfixLessOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return new EvaluationValue(operands[0].compareTo(operands[1]) < 0); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixNotEqualsOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixNotEqualsOperator.java new file mode 100644 index 0000000..5f0d386 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixNotEqualsOperator.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_EQUALITY; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +/** No equality of two values. */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_EQUALITY) +public class InfixNotEqualsOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return new EvaluationValue(!operands[0].equals(operands[1])); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixOrOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixOrOperator.java new file mode 100644 index 0000000..dc4a8bc --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixOrOperator.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_OR; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.parser.Token; + +/** Boolean OR of two values. */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_OR) +public class InfixOrOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return new EvaluationValue(operands[0].getBooleanValue() || operands[1].getBooleanValue()); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/PrefixNotOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/PrefixNotOperator.java new file mode 100644 index 0000000..aa95851 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/PrefixNotOperator.java @@ -0,0 +1,33 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.PrefixOperator; +import com.ezylang.evalex.parser.Token; + +/** Boolean negation of value. */ +@PrefixOperator +public class PrefixNotOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return new EvaluationValue(!operands[0].getBooleanValue()); + } +} diff --git a/src/main/java/com/ezylang/evalex/parser/ASTNode.java b/src/main/java/com/ezylang/evalex/parser/ASTNode.java new file mode 100644 index 0000000..984032e --- /dev/null +++ b/src/main/java/com/ezylang/evalex/parser/ASTNode.java @@ -0,0 +1,68 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import lombok.Value; + +/** + * Expressions are parsed into an abstract syntax tree (AST). The tree has one root node and each + * node has zero or more children (parameters), depending on the operation. A leaf node is a + * numerical or string constant that has no more children (parameters). Other nodes define + * operators, functions and special operations like array index and structure separation. + * + *

The tree is evaluated from bottom (leafs) to top, in a recursive way, until the root node is + * evaluated, which then holds the result of the complete expression. + * + *

To be able to visualize the tree, a toJSON method is provided. The produced JSON + * string can be used to visualize the tree. OE.g. with this online tool: + * + *

Online JSON to Tree Diagram Converter + */ +@Value +public class ASTNode { + + /** The children od the tree. */ + List parameters; + + /** The token associated with this tree node. */ + Token token; + + public ASTNode(Token token, ASTNode... parameters) { + this.token = token; + this.parameters = Arrays.asList(parameters); + } + + /** + * Produces a JSON string representation of this node ad all its children. + * + * @return A JSON string of the tree structure starting at this node. + */ + public String toJSON() { + if (parameters.isEmpty()) { + return String.format( + "{" + "\"type\":\"%s\",\"value\":\"%s\"}", token.getType(), token.getValue()); + } else { + String childrenJson = + parameters.stream().map(ASTNode::toJSON).collect(Collectors.joining(",")); + return String.format( + "{" + "\"type\":\"%s\",\"value\":\"%s\",\"children\":[%s]}", + token.getType(), token.getValue(), childrenJson); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/parser/ParseException.java b/src/main/java/com/ezylang/evalex/parser/ParseException.java new file mode 100644 index 0000000..6fd085b --- /dev/null +++ b/src/main/java/com/ezylang/evalex/parser/ParseException.java @@ -0,0 +1,40 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import com.ezylang.evalex.BaseException; +import lombok.ToString; + +/** Exception while parsing the expression. */ +@ToString(callSuper = true) +public class ParseException extends BaseException { + + public ParseException(int startPosition, int endPosition, String tokenString, String message) { + super(startPosition, endPosition, tokenString, message); + } + + public ParseException(String expression, String message) { + super(1, expression.length(), expression, message); + } + + public ParseException(Token token, String message) { + super( + token.getStartPosition(), + token.getStartPosition() + token.getValue().length() - 1, + token.getValue(), + message); + } +} diff --git a/src/main/java/com/ezylang/evalex/parser/ShuntingYardConverter.java b/src/main/java/com/ezylang/evalex/parser/ShuntingYardConverter.java new file mode 100644 index 0000000..9c8fd06 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/parser/ShuntingYardConverter.java @@ -0,0 +1,287 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import static com.ezylang.evalex.parser.Token.TokenType.ARRAY_INDEX; +import static com.ezylang.evalex.parser.Token.TokenType.ARRAY_OPEN; +import static com.ezylang.evalex.parser.Token.TokenType.BRACE_OPEN; +import static com.ezylang.evalex.parser.Token.TokenType.FUNCTION; +import static com.ezylang.evalex.parser.Token.TokenType.STRUCTURE_SEPARATOR; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.functions.FunctionIfc; +import com.ezylang.evalex.operators.OperatorIfc; +import com.ezylang.evalex.parser.Token.TokenType; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +/** + * The shunting yard algorithm can be used to convert a mathematical expression from an infix + * notation into either a postfix notation (RPN, reverse polish notation), or into an abstract + * syntax tree (AST). + * + *

Here it is used to parse and convert a list of already parsed expression tokens into an AST. + * + * @see Shunting yard algorithm + * @see Abstract syntax tree + */ +public class ShuntingYardConverter { + + private final List expressionTokens; + + private final String originalExpression; + + private final ExpressionConfiguration configuration; + + private final Deque operatorStack = new ArrayDeque<>(); + private final Deque operandStack = new ArrayDeque<>(); + + public ShuntingYardConverter( + String originalExpression, + List expressionTokens, + ExpressionConfiguration configuration) { + this.originalExpression = originalExpression; + this.expressionTokens = expressionTokens; + this.configuration = configuration; + } + + public ASTNode toAbstractSyntaxTree() throws ParseException { + + Token previousToken = null; + for (Token currentToken : expressionTokens) { + switch (currentToken.getType()) { + case VARIABLE_OR_CONSTANT: + case NUMBER_LITERAL: + case STRING_LITERAL: + operandStack.push(new ASTNode(currentToken)); + break; + case FUNCTION: + operatorStack.push(currentToken); + break; + case COMMA: + processOperatorsFromStackUntilTokenType(BRACE_OPEN); + break; + case INFIX_OPERATOR: + case PREFIX_OPERATOR: + case POSTFIX_OPERATOR: + processOperator(currentToken); + break; + case BRACE_OPEN: + processBraceOpen(previousToken, currentToken); + break; + case BRACE_CLOSE: + processBraceClose(); + break; + case ARRAY_OPEN: + processArrayOpen(currentToken); + break; + case ARRAY_CLOSE: + processArrayClose(); + break; + case STRUCTURE_SEPARATOR: + processStructureSeparator(currentToken); + break; + default: + throw new ParseException( + currentToken, "Unexpected token of type '" + currentToken.getType() + "'"); + } + previousToken = currentToken; + } + + while (!operatorStack.isEmpty()) { + Token token = operatorStack.pop(); + createOperatorNode(token); + } + + if (operandStack.isEmpty()) { + throw new ParseException(this.originalExpression, "Empty expression"); + } + + return operandStack.pop(); + } + + private void processStructureSeparator(Token currentToken) throws ParseException { + Token nextToken = operatorStack.isEmpty() ? null : operatorStack.peek(); + while (nextToken != null && nextToken.getType() == STRUCTURE_SEPARATOR) { + Token token = operatorStack.pop(); + createOperatorNode(token); + nextToken = operatorStack.isEmpty() ? null : operatorStack.peek(); + } + operatorStack.push(currentToken); + } + + private void processBraceOpen(Token previousToken, Token currentToken) { + if (previousToken != null && previousToken.getType() == FUNCTION) { + // start of parameter list, marker for variable number of arguments + Token paramStart = + new Token( + currentToken.getStartPosition(), + currentToken.getValue(), + TokenType.FUNCTION_PARAM_START); + operandStack.push(new ASTNode(paramStart)); + } + operatorStack.push(currentToken); + } + + private void processBraceClose() throws ParseException { + processOperatorsFromStackUntilTokenType(BRACE_OPEN); + operatorStack.pop(); // throw away the marker + if (!operatorStack.isEmpty() && operatorStack.peek().getType() == FUNCTION) { + Token functionToken = operatorStack.pop(); + ArrayList parameters = new ArrayList<>(); + while (!operandStack.isEmpty()) { + // add all parameters in reverse order from stack to the parameter array + ASTNode node = operandStack.pop(); + if (node.getToken().getType() == TokenType.FUNCTION_PARAM_START) { + break; + } + parameters.add(0, node); + } + validateFunctionParameters(functionToken, parameters); + operandStack.push(new ASTNode(functionToken, parameters.toArray(new ASTNode[0]))); + } + } + + private void validateFunctionParameters(Token functionToken, ArrayList parameters) + throws ParseException { + FunctionIfc function = functionToken.getFunctionDefinition(); + if (parameters.size() < function.getFunctionParameterDefinitions().size()) { + throw new ParseException(functionToken, "Not enough parameters for function"); + } + if (!function.hasVarArgs() + && parameters.size() > function.getFunctionParameterDefinitions().size()) { + throw new ParseException(functionToken, "Too many parameters for function"); + } + } + + /** + * Array index is treated like a function with two parameters. First parameter is the array (name + * or evaluation result). Second parameter is the array index. + * + * @param currentToken The current ARRAY_OPEN ("[") token. + */ + private void processArrayOpen(Token currentToken) throws ParseException { + Token nextToken = operatorStack.isEmpty() ? null : operatorStack.peek(); + while (nextToken != null && (nextToken.getType() == STRUCTURE_SEPARATOR)) { + Token token = operatorStack.pop(); + createOperatorNode(token); + nextToken = operatorStack.isEmpty() ? null : operatorStack.peek(); + } + // create ARRAY_INDEX operator (just like a function name) and push it to the operator stack + Token arrayIndex = + new Token(currentToken.getStartPosition(), currentToken.getValue(), ARRAY_INDEX); + operatorStack.push(arrayIndex); + + // push the ARRAY_OPEN to the operators, too (to later match the ARRAY_CLOSE) + operatorStack.push(currentToken); + } + + /** + * Follows the logic for a function, but with two fixed parameters. + * + * @throws ParseException If there were problems while processing the stacks. + */ + private void processArrayClose() throws ParseException { + processOperatorsFromStackUntilTokenType(ARRAY_OPEN); + operatorStack.pop(); // throw away the marker + Token arrayToken = operatorStack.pop(); + ArrayList operands = new ArrayList<>(); + + // second parameter of the "ARRAY_INDEX" function is the index (first on stack) + ASTNode index = operandStack.pop(); + operands.add(0, index); + + // first parameter of the "ARRAY_INDEX" function is the array (name or evaluation result) + // (second on stack) + ASTNode array = operandStack.pop(); + operands.add(0, array); + + operandStack.push(new ASTNode(arrayToken, operands.toArray(new ASTNode[0]))); + } + + private void processOperatorsFromStackUntilTokenType(TokenType untilTokenType) + throws ParseException { + while (!operatorStack.isEmpty() && operatorStack.peek().getType() != untilTokenType) { + Token token = operatorStack.pop(); + createOperatorNode(token); + } + } + + private void createOperatorNode(Token token) throws ParseException { + if (operandStack.isEmpty()) { + throw new ParseException(token, "Missing operand for operator"); + } + + ASTNode operand1 = operandStack.pop(); + + if (token.getType() == TokenType.PREFIX_OPERATOR + || token.getType() == TokenType.POSTFIX_OPERATOR) { + operandStack.push(new ASTNode(token, operand1)); + } else { + if (operandStack.isEmpty()) { + throw new ParseException(token, "Missing second operand for operator"); + } + ASTNode operand2 = operandStack.pop(); + operandStack.push(new ASTNode(token, operand2, operand1)); + } + } + + private void processOperator(Token currentToken) throws ParseException { + Token nextToken = operatorStack.isEmpty() ? null : operatorStack.peek(); + while (isOperator(nextToken) + && isNextOperatorOfHigherPrecedence( + currentToken.getOperatorDefinition(), nextToken.getOperatorDefinition())) { + Token token = operatorStack.pop(); + createOperatorNode(token); + nextToken = operatorStack.isEmpty() ? null : operatorStack.peek(); + } + operatorStack.push(currentToken); + } + + private boolean isNextOperatorOfHigherPrecedence( + OperatorIfc currentOperator, OperatorIfc nextOperator) { + // structure operator (null) has always a higher precedence than other operators + if (nextOperator == null) { + return true; + } + + if (currentOperator.isLeftAssociative()) { + return currentOperator.getPrecedence(configuration) + <= nextOperator.getPrecedence(configuration); + } else { + return currentOperator.getPrecedence(configuration) + < nextOperator.getPrecedence(configuration); + } + } + + private boolean isOperator(Token token) { + if (token == null) { + return false; + } + TokenType tokenType = token.getType(); + switch (tokenType) { + case INFIX_OPERATOR: + case PREFIX_OPERATOR: + case POSTFIX_OPERATOR: + case STRUCTURE_SEPARATOR: + return true; + default: + return false; + } + } +} diff --git a/src/main/java/com/ezylang/evalex/parser/Token.java b/src/main/java/com/ezylang/evalex/parser/Token.java new file mode 100644 index 0000000..82e6f1d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/parser/Token.java @@ -0,0 +1,76 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import com.ezylang.evalex.functions.FunctionIfc; +import com.ezylang.evalex.operators.OperatorIfc; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.Value; + +/** + * A token represents a singe part of an expression, like an operator, number literal, or a brace. + * Each token has a unique type, a value (its representation) and a position (starting with 1) in + * the original expression string. + * + *

For operators and functions, the operator and function definition is also set during parsing. + */ +@Value +@AllArgsConstructor +@EqualsAndHashCode(doNotUseGetters = true) +public class Token { + + public enum TokenType { + BRACE_OPEN, + BRACE_CLOSE, + COMMA, + STRING_LITERAL, + NUMBER_LITERAL, + VARIABLE_OR_CONSTANT, + INFIX_OPERATOR, + PREFIX_OPERATOR, + POSTFIX_OPERATOR, + FUNCTION, + FUNCTION_PARAM_START, + ARRAY_OPEN, + ARRAY_CLOSE, + ARRAY_INDEX, + STRUCTURE_SEPARATOR + } + + int startPosition; + + String value; + + TokenType type; + + @EqualsAndHashCode.Exclude @ToString.Exclude FunctionIfc functionDefinition; + + @EqualsAndHashCode.Exclude @ToString.Exclude OperatorIfc operatorDefinition; + + public Token(int startPosition, String value, TokenType type) { + this(startPosition, value, type, null, null); + } + + public Token(int startPosition, String value, TokenType type, FunctionIfc functionDefinition) { + this(startPosition, value, type, functionDefinition, null); + } + + public Token(int startPosition, String value, TokenType type, OperatorIfc operatorDefinition) { + this(startPosition, value, type, null, operatorDefinition); + } +} diff --git a/src/main/java/com/ezylang/evalex/parser/Tokenizer.java b/src/main/java/com/ezylang/evalex/parser/Tokenizer.java new file mode 100644 index 0000000..7fa3788 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/parser/Tokenizer.java @@ -0,0 +1,507 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import static com.ezylang.evalex.parser.Token.TokenType.BRACE_OPEN; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.config.FunctionDictionaryIfc; +import com.ezylang.evalex.config.OperatorDictionaryIfc; +import com.ezylang.evalex.functions.FunctionIfc; +import com.ezylang.evalex.operators.OperatorIfc; +import com.ezylang.evalex.parser.Token.TokenType; +import java.util.ArrayList; +import java.util.List; + +/** + * The tokenizer is responsible to parse a string and return a list of tokens. The order of tokens + * will follow the infix expression notation, skipping any blank characters. + */ +public class Tokenizer { + + private final String expressionString; + + private final OperatorDictionaryIfc operatorDictionary; + + private final FunctionDictionaryIfc functionDictionary; + + private final ExpressionConfiguration configuration; + + private final List tokens = new ArrayList<>(); + + private int currentColumnIndex = 0; + + private int currentChar = -2; + + private int braceBalance; + + private int arrayBalance; + + public Tokenizer(String expressionString, ExpressionConfiguration configuration) { + this.expressionString = expressionString; + this.configuration = configuration; + this.operatorDictionary = configuration.getOperatorDictionary(); + this.functionDictionary = configuration.getFunctionDictionary(); + } + + /** + * Parse the given expression and return a list of tokens, representing the expression. + * + * @return A list of expression tokens. + * @throws ParseException When the expression can't be parsed. + */ + public List parse() throws ParseException { + Token currentToken = getNextToken(); + while (currentToken != null) { + if (currentToken.getType() == BRACE_OPEN && implicitMultiplicationPossible()) { + if (configuration.isImplicitMultiplicationAllowed()) { + Token multiplication = + new Token(currentToken.getStartPosition(), "*", TokenType.INFIX_OPERATOR); + tokens.add(multiplication); + } else { + throw new ParseException(currentToken, "Missing operator"); + } + } + tokens.add(currentToken); + currentToken = getNextToken(); + } + + if (braceBalance > 0) { + throw new ParseException(expressionString, "Closing brace not found"); + } + + if (arrayBalance > 0) { + throw new ParseException(expressionString, "Closing array not found"); + } + + return tokens; + } + + private Token getNextToken() throws ParseException { + + // blanks are always skipped. + skipBlanks(); + + // end of input + if (currentChar == -1) { + return null; + } + + // we have a token start, identify and parse it + if (currentChar == '"') { + return parseStringLiteral(); + } else if (currentChar == '(') { + return parseBraceOpen(); + } else if (currentChar == ')') { + return parseBraceClose(); + } else if (currentChar == '[' && configuration.isArraysAllowed()) { + return parseArrayOpen(); + } else if (currentChar == ']' && configuration.isArraysAllowed()) { + return parseArrayClose(); + } else if (currentChar == '.' + && !isNumberChar(peekNextChar()) + && configuration.isStructuresAllowed()) { + return parseStructureSeparator(); + } else if (currentChar == ',') { + Token token = new Token(currentColumnIndex, ",", TokenType.COMMA); + consumeChar(); + return token; + } else if (isIdentifierStart(currentChar)) { + return parseIdentifier(); + } else if (isNumberStart(currentChar)) { + return parseNumberLiteral(); + } else { + return parseOperator(); + } + } + + private Token parseStructureSeparator() throws ParseException { + Token token = new Token(currentColumnIndex, ".", TokenType.STRUCTURE_SEPARATOR); + if (arrayOpenOrStructureSeparatorNotAllowed()) { + throw new ParseException(token, "Structure separator not allowed here"); + } + consumeChar(); + return token; + } + + private Token parseArrayClose() throws ParseException { + Token token = new Token(currentColumnIndex, "]", TokenType.ARRAY_CLOSE); + if (!arrayCloseAllowed()) { + throw new ParseException(token, "Array close not allowed here"); + } + consumeChar(); + arrayBalance--; + if (arrayBalance < 0) { + throw new ParseException(token, "Unexpected closing array"); + } + return token; + } + + private Token parseArrayOpen() throws ParseException { + Token token = new Token(currentColumnIndex, "[", TokenType.ARRAY_OPEN); + if (arrayOpenOrStructureSeparatorNotAllowed()) { + throw new ParseException(token, "Array open not allowed here"); + } + consumeChar(); + arrayBalance++; + return token; + } + + private Token parseBraceClose() throws ParseException { + Token token = new Token(currentColumnIndex, ")", TokenType.BRACE_CLOSE); + consumeChar(); + braceBalance--; + if (braceBalance < 0) { + throw new ParseException(token, "Unexpected closing brace"); + } + return token; + } + + private Token parseBraceOpen() { + Token token = new Token(currentColumnIndex, "(", BRACE_OPEN); + consumeChar(); + braceBalance++; + return token; + } + + private Token getPreviousToken() { + return tokens.isEmpty() ? null : tokens.get(tokens.size() - 1); + } + + private Token parseOperator() throws ParseException { + int tokenStartIndex = currentColumnIndex; + StringBuilder tokenValue = new StringBuilder(); + while (currentChar != -1 && isNotOtherTokenStart(currentChar)) { + tokenValue.append((char) currentChar); + String tokenString = tokenValue.toString(); + String possibleNextOperator = tokenString + (char) peekNextChar(); + boolean possibleNextOperatorFound = + (prefixOperatorAllowed() && operatorDictionary.hasPrefixOperator(possibleNextOperator)) + || (postfixOperatorAllowed() + && operatorDictionary.hasPostfixOperator(possibleNextOperator)) + || (infixOperatorAllowed() + && operatorDictionary.hasInfixOperator(possibleNextOperator)); + consumeChar(); + if (!possibleNextOperatorFound) { + break; + } + } + String tokenString = tokenValue.toString(); + if (prefixOperatorAllowed() && operatorDictionary.hasPrefixOperator(tokenString)) { + OperatorIfc operator = operatorDictionary.getPrefixOperator(tokenString); + return new Token(tokenStartIndex, tokenString, TokenType.PREFIX_OPERATOR, operator); + } else if (postfixOperatorAllowed() && operatorDictionary.hasPostfixOperator(tokenString)) { + OperatorIfc operator = operatorDictionary.getPostfixOperator(tokenString); + return new Token(tokenStartIndex, tokenString, TokenType.POSTFIX_OPERATOR, operator); + } else if (operatorDictionary.hasInfixOperator(tokenString)) { + OperatorIfc operator = operatorDictionary.getInfixOperator(tokenString); + return new Token(tokenStartIndex, tokenString, TokenType.INFIX_OPERATOR, operator); + } + throw new ParseException( + tokenStartIndex, + tokenStartIndex + tokenString.length() - 1, + tokenString, + "Undefined operator '" + tokenString + "'"); + } + + private boolean implicitMultiplicationPossible() { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return false; + } + + switch (previousToken.getType()) { + case BRACE_CLOSE: + case NUMBER_LITERAL: + return true; + default: + return false; + } + } + + private boolean arrayOpenOrStructureSeparatorNotAllowed() { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return true; + } + + switch (previousToken.getType()) { + case BRACE_CLOSE: + case VARIABLE_OR_CONSTANT: + case ARRAY_CLOSE: + return false; + default: + return true; + } + } + + private boolean arrayCloseAllowed() { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return false; + } + + switch (previousToken.getType()) { + case BRACE_OPEN: + case INFIX_OPERATOR: + case PREFIX_OPERATOR: + case FUNCTION: + case COMMA: + case ARRAY_OPEN: + return false; + default: + return true; + } + } + + private boolean prefixOperatorAllowed() { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return true; + } + + switch (previousToken.getType()) { + case BRACE_OPEN: + case INFIX_OPERATOR: + case COMMA: + case PREFIX_OPERATOR: + return true; + default: + return false; + } + } + + private boolean postfixOperatorAllowed() { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return false; + } + + switch (previousToken.getType()) { + case BRACE_CLOSE: + case NUMBER_LITERAL: + case VARIABLE_OR_CONSTANT: + case STRING_LITERAL: + return true; + default: + return false; + } + } + + private boolean infixOperatorAllowed() { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return false; + } + + switch (previousToken.getType()) { + case BRACE_CLOSE: + case VARIABLE_OR_CONSTANT: + case STRING_LITERAL: + case POSTFIX_OPERATOR: + case NUMBER_LITERAL: + return true; + default: + return false; + } + } + + private Token parseNumberLiteral() { + int tokenStartIndex = currentColumnIndex; + StringBuilder tokenValue = new StringBuilder(); + int nextChar = peekNextChar(); + if (currentChar == '0' && (nextChar == 'x' || nextChar == 'X')) { + // hexadecimal number, consume "0x" + tokenValue.append((char) currentChar); + consumeChar(); + tokenValue.append((char) currentChar); + consumeChar(); + while (currentChar != -1 && isHexChar(currentChar)) { + tokenValue.append((char) currentChar); + consumeChar(); + } + } else { + // decimal number + while (currentChar != -1 && isNumberChar(currentChar)) { + tokenValue.append((char) currentChar); + consumeChar(); + } + } + return new Token(tokenStartIndex, tokenValue.toString(), TokenType.NUMBER_LITERAL); + } + + private Token parseIdentifier() throws ParseException { + int tokenStartIndex = currentColumnIndex; + StringBuilder tokenValue = new StringBuilder(); + while (currentChar != -1 && isIdentifierChar(currentChar)) { + tokenValue.append((char) currentChar); + consumeChar(); + } + String tokenName = tokenValue.toString(); + + if (prefixOperatorAllowed() && operatorDictionary.hasPrefixOperator(tokenName)) { + return new Token( + tokenStartIndex, + tokenName, + TokenType.PREFIX_OPERATOR, + operatorDictionary.getPrefixOperator(tokenName)); + } else if (postfixOperatorAllowed() && operatorDictionary.hasPostfixOperator(tokenName)) { + return new Token( + tokenStartIndex, + tokenName, + TokenType.POSTFIX_OPERATOR, + operatorDictionary.getPostfixOperator(tokenName)); + } else if (infixOperatorAllowed() && operatorDictionary.hasInfixOperator(tokenName)) { + return new Token( + tokenStartIndex, + tokenName, + TokenType.INFIX_OPERATOR, + operatorDictionary.getInfixOperator(tokenName)); + } + + skipBlanks(); + if (currentChar == '(') { + if (!functionDictionary.hasFunction(tokenName)) { + throw new ParseException( + tokenStartIndex, + currentColumnIndex, + tokenName, + "Undefined function '" + tokenName + "'"); + } + FunctionIfc function = functionDictionary.getFunction(tokenName); + return new Token(tokenStartIndex, tokenName, TokenType.FUNCTION, function); + } else { + return new Token(tokenStartIndex, tokenName, TokenType.VARIABLE_OR_CONSTANT); + } + } + + Token parseStringLiteral() throws ParseException { + int tokenStartIndex = currentColumnIndex; + StringBuilder tokenValue = new StringBuilder(); + // skip starting quote + consumeChar(); + while (currentChar != -1 && currentChar != '"' && peekPreviousChar() != '\\') { + tokenValue.append((char) currentChar); + consumeChar(); + } + // skip trailing quote + if (currentChar == '"') { + consumeChar(); + } else { + throw new ParseException( + tokenStartIndex, currentColumnIndex, tokenValue.toString(), "Closing quote not found"); + } + return new Token(tokenStartIndex, tokenValue.toString(), TokenType.STRING_LITERAL); + } + + private boolean isNotOtherTokenStart(int ch) { + return !(Character.isWhitespace(ch) + || isNumberStart(ch) + || isIdentifierStart(ch) + || ch == '"' + || ch == '(' + || ch == ')' + || ch == ','); + } + + private boolean isNumberStart(int ch) { + if (Character.isDigit(ch)) { + return true; + } + return ch == '.' && Character.isDigit(peekNextChar()); + } + + private boolean isNumberChar(int ch) { + int previousChar = peekPreviousChar(); + if (previousChar == 'e' || previousChar == 'E') { + return Character.isDigit(ch) || ch == '.' || ch == '+' || ch == '-'; + } else { + return Character.isDigit(ch) || ch == '.' || ch == 'e' || ch == 'E'; + } + } + + private boolean isHexChar(int ch) { + switch (ch) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + return true; + default: + return false; + } + } + + private boolean isIdentifierStart(int ch) { + return Character.isLetter(ch) || ch == '_'; + } + + private boolean isIdentifierChar(int ch) { + return Character.isLetter(ch) || Character.isDigit(ch) || ch == '_'; + } + + private void skipBlanks() { + if (currentChar == -2) { + // consume first character of expression + consumeChar(); + } + while (currentChar != -1 && Character.isWhitespace(currentChar)) { + consumeChar(); + } + } + + private int peekNextChar() { + return currentColumnIndex == expressionString.length() + ? -1 + : expressionString.charAt(currentColumnIndex); + } + + private int peekPreviousChar() { + return currentColumnIndex == 1 ? -1 : expressionString.charAt(currentColumnIndex - 2); + } + + private void consumeChar() { + if (currentColumnIndex == expressionString.length()) { + currentChar = -1; + } else { + currentChar = expressionString.charAt(currentColumnIndex++); + } + } +} diff --git a/src/main/java/com/udojava/evalex/AbstractFunction.java b/src/main/java/com/udojava/evalex/AbstractFunction.java deleted file mode 100644 index 44fcf65..0000000 --- a/src/main/java/com/udojava/evalex/AbstractFunction.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2018 Udo Klimaschewski - * - * http://UdoJava.com/ - * http://about.me/udo.klimaschewski - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - */ -package com.udojava.evalex; - -import com.udojava.evalex.Expression.LazyNumber; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; - -/** - * Abstract implementation of a direct function.
- *
- * This abstract implementation does implement lazyEval so that it returns the result of eval. - */ -public abstract class AbstractFunction extends AbstractLazyFunction implements Function { - - /** - * Creates a new function with given name and parameter count. - * - * @param name The name of the function. - * @param numParams The number of parameters for this function. - * -1 denotes a variable number of parameters. - */ - protected AbstractFunction(String name, int numParams) { - super(name, numParams); - } - - /** - * Creates a new function with given name and parameter count. - * - * @param name The name of the function. - * @param numParams The number of parameters for this function. - * -1 denotes a variable number of parameters. - * @param booleanFunction Whether this function is a boolean function. - */ - protected AbstractFunction(String name, int numParams, boolean booleanFunction) { - super(name, numParams, booleanFunction); - } - - public LazyNumber lazyEval(final List lazyParams) { - return new LazyNumber() { - - private List params; - - public BigDecimal eval() { - return AbstractFunction.this.eval(getParams()); - } - - public String getString() { - return String.valueOf(AbstractFunction.this.eval(getParams())); - } - - private List getParams() { - if (params == null) { - params = new ArrayList(); - for (LazyNumber lazyParam : lazyParams) { - params.add(lazyParam.eval()); - } - } - return params; - } - }; - } -} diff --git a/src/main/java/com/udojava/evalex/AbstractLazyFunction.java b/src/main/java/com/udojava/evalex/AbstractLazyFunction.java deleted file mode 100644 index 5aa2034..0000000 --- a/src/main/java/com/udojava/evalex/AbstractLazyFunction.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2018 Udo Klimaschewski - * - * http://UdoJava.com/ - * http://about.me/udo.klimaschewski - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - */ -package com.udojava.evalex; - -import java.util.Locale; - -/** - * Abstract implementation of a lazy function which implements all necessary methods with the - * exception of the main logic. - */ -public abstract class AbstractLazyFunction implements LazyFunction { - - /** - * Name of this function. - */ - protected String name; - /** - * Number of parameters expected for this function. -1 denotes a variable number of - * parameters. - */ - protected int numParams; - - /** - * Whether this function is a boolean function. - */ - protected boolean booleanFunction; - - /** - * Creates a new function with given name and parameter count. - * - * @param name The name of the function. - * @param numParams The number of parameters for this function. - * -1 denotes a variable number of parameters. - * @param booleanFunction Whether this function is a boolean function. - */ - protected AbstractLazyFunction(String name, int numParams, boolean booleanFunction) { - this.name = name.toUpperCase(Locale.ROOT); - this.numParams = numParams; - this.booleanFunction = booleanFunction; - } - - /** - * Creates a new function with given name and parameter count. - * - * @param name The name of the function. - * @param numParams The number of parameters for this function. - * -1 denotes a variable number of parameters. - */ - protected AbstractLazyFunction(String name, int numParams) { - this(name, numParams, false); - } - - public String getName() { - return name; - } - - public int getNumParams() { - return numParams; - } - - public boolean numParamsVaries() { - return numParams < 0; - } - - public boolean isBooleanFunction() { - return booleanFunction; - } -} diff --git a/src/main/java/com/udojava/evalex/AbstractLazyOperator.java b/src/main/java/com/udojava/evalex/AbstractLazyOperator.java deleted file mode 100644 index c4e175a..0000000 --- a/src/main/java/com/udojava/evalex/AbstractLazyOperator.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 2018 Udo Klimaschewski - * - * http://UdoJava.com/ - * http://about.me/udo.klimaschewski - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - */ -package com.udojava.evalex; - -/** - * Abstract implementation of an operator. - */ -public abstract class AbstractLazyOperator implements LazyOperator { - - - /** - * This operators name (pattern). - */ - protected String oper; - - /** - * Operators precedence. - */ - protected int precedence; - - /** - * Operator is left associative. - */ - protected boolean leftAssoc; - - /** - * Operator is boolean. - */ - protected boolean booleanOperator = false; - - /** - * Operator is unary, i.e. 1 or 2 operands expected for this operator. - */ - protected boolean unaryOperator; - - - /** - * Creates a new unary operator. This constructor allows the use of postfix unary operators. - * - * @param oper The operator name (pattern). - * @param precedence The operators precedence. - * @param leftAssoc true if the operator is left associative, - * else false. - * @param booleanOperator Whether this operator is boolean. - * @param unaryOperator true if the expected number of operands is 1, - * false if the expected number of operands is 2. - */ - protected AbstractLazyOperator(String oper, int precedence, boolean leftAssoc, - boolean booleanOperator, boolean unaryOperator) { - this.oper = oper; - this.precedence = precedence; - this.leftAssoc = leftAssoc; - this.booleanOperator = booleanOperator; - this.unaryOperator = unaryOperator; - } - - - /** - * Creates a new boolean operator. - * - * @param oper The operator name (pattern). - * @param precedence The operators precedence. - * @param leftAssoc true if the operator is left associative, - * else false. - * @param booleanOperator Whether this operator is boolean. - */ - protected AbstractLazyOperator(String oper, int precedence, boolean leftAssoc, - boolean booleanOperator) { - this.oper = oper; - this.precedence = precedence; - this.leftAssoc = leftAssoc; - this.booleanOperator = booleanOperator; - this.unaryOperator = false; - } - - - /** - * Creates a new operator. - * - * @param oper The operator name (pattern). - * @param precedence The operators precedence. - * @param leftAssoc true if the operator is left associative, - * else false. - */ - protected AbstractLazyOperator(String oper, int precedence, boolean leftAssoc) { - this.oper = oper; - this.precedence = precedence; - this.leftAssoc = leftAssoc; - this.unaryOperator = false; - } - - - /** - * @return The String that is used to denote the operator in the expression. - */ - public String getOper() { - return oper; - } - - - /** - * @return the precedence value of this operator. - */ - public int getPrecedence() { - return precedence; - } - - - /** - * @return true if this operator is left associative. - */ - public boolean isLeftAssoc() { - return leftAssoc; - } - - - /** - * @return true if this operator evaluates to a boolean - * expression. - */ - public boolean isBooleanOperator() { - return booleanOperator; - } - - - /** - * @return true if the number of operands for this operator is 1, - * or false if the number of operands is 2. - */ - public boolean isUnaryOperator() { - return unaryOperator; - } - -} diff --git a/src/main/java/com/udojava/evalex/AbstractOperator.java b/src/main/java/com/udojava/evalex/AbstractOperator.java deleted file mode 100644 index d566ce3..0000000 --- a/src/main/java/com/udojava/evalex/AbstractOperator.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2018 Udo Klimaschewski - * - * http://UdoJava.com/ - * http://about.me/udo.klimaschewski - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - */ -package com.udojava.evalex; - -import com.udojava.evalex.Expression.LazyNumber; -import java.math.BigDecimal; - -/** - * Abstract implementation of an operator. - */ -public abstract class AbstractOperator extends AbstractLazyOperator implements Operator { - - - /** - * Creates a new unary operator. - * - * @param oper The operator name (pattern). - * @param precedence The operators precedence. - * @param leftAssoc true if the operator is left associative, - * else false. - * @param booleanOperator Whether this operator is boolean. - * @param unaryOperator Whether the operator is unary (true) or not - * (false). - */ - protected AbstractOperator(String oper, int precedence, boolean leftAssoc, - boolean booleanOperator, boolean unaryOperator) { - super(oper, precedence, leftAssoc, booleanOperator, unaryOperator); - } - - - /** - * Creates a new boolean operator. - * - * @param oper The operator name (pattern). - * @param precedence The operators precedence. - * @param leftAssoc true if the operator is left associative, - * else false. - * @param booleanOperator Whether this operator is boolean. - */ - protected AbstractOperator(String oper, int precedence, boolean leftAssoc, - boolean booleanOperator) { - super(oper, precedence, leftAssoc, booleanOperator); - } - - - /** - * Creates a new operator. - * - * @param oper The operator name (pattern). - * @param precedence The operators precedence. - * @param leftAssoc true if the operator is left associative, - * else false. - */ - protected AbstractOperator(String oper, int precedence, boolean leftAssoc) { - super(oper, precedence, leftAssoc); - } - - - /** - * Implementation of this operator supporting either 1 or 2 operands. - * - * @param v1 The first operand expected for the operation. - * @param v2 The second operand expected for the operation. For postfix unary operators, v2=null - * condition was added. - * @return LazyNumber object. The result of the operation. - */ - public LazyNumber eval(final LazyNumber v1, final LazyNumber v2) { - if (v2 == null) { // Condition to accept postfix unary operators (e.g. factorial operator '!') - return new LazyNumber() { - public BigDecimal eval() { - return AbstractOperator.this.eval(v1.eval(), null); - } - - public String getString() { - return String.valueOf(AbstractOperator.this.eval(v1.eval(), null)); - } - }; - } else { - return new LazyNumber() { - public BigDecimal eval() { - return AbstractOperator.this.eval(v1.eval(), v2.eval()); - } - - public String getString() { - return String.valueOf(AbstractOperator.this.eval(v1.eval(), v2.eval())); - } - }; - } - } - -} diff --git a/src/main/java/com/udojava/evalex/AbstractUnaryOperator.java b/src/main/java/com/udojava/evalex/AbstractUnaryOperator.java deleted file mode 100644 index 68e0f23..0000000 --- a/src/main/java/com/udojava/evalex/AbstractUnaryOperator.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2018 Udo Klimaschewski - * - * http://UdoJava.com/ - * http://about.me/udo.klimaschewski - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - */ -package com.udojava.evalex; - -import com.udojava.evalex.Expression.ExpressionException; -import com.udojava.evalex.Expression.LazyNumber; -import java.math.BigDecimal; - -/** - * Abstract implementation of an unary operator.
- *
- * This abstract implementation implements eval so that it forwards its first parameter to - * evalUnary. - */ -public abstract class AbstractUnaryOperator extends AbstractOperator { - - - /** - * Creates a new operator. - * - * @param oper The operator name (pattern). - * @param precedence The operators precedence. - * @param leftAssoc true if the operator is left associative, - * else false. - */ - protected AbstractUnaryOperator(String oper, int precedence, boolean leftAssoc) { - super(oper, precedence, leftAssoc); - } - - - @Override - public LazyNumber eval(final LazyNumber v1, final LazyNumber v2) { - if (v2 != null) { - throw new ExpressionException("Did not expect a second parameter for unary operator"); - } - return new LazyNumber() { - @Override - public String getString() { - return String.valueOf(AbstractUnaryOperator.this.evalUnary(v1.eval())); - } - - @Override - public BigDecimal eval() { - return AbstractUnaryOperator.this.evalUnary(v1.eval()); - } - }; - } - - - /** - * Implementation of this operator calling unary operator evaluation method.. - * - * @param v1 The first parameter. - * @param v2 The second parameter. Expected to be null - * @return The result of the operation. - */ - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - if (v2 != null) { - throw new ExpressionException("Did not expect a second parameter for unary operator"); - } - return evalUnary(v1); - } - - - /** - * Implementation of this prefix unary operator. - * - * @param v1 The parameter. - * @return The result of the operation. - */ - public abstract BigDecimal evalUnary(BigDecimal v1); -} diff --git a/src/main/java/com/udojava/evalex/Expression.java b/src/main/java/com/udojava/evalex/Expression.java deleted file mode 100644 index a3c33d8..0000000 --- a/src/main/java/com/udojava/evalex/Expression.java +++ /dev/null @@ -1,2211 +0,0 @@ -/* - * Copyright 2012-2020 Udo Klimaschewski - * - * http://UdoJava.com/ - * http://about.me/udo.klimaschewski - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - */ -package com.udojava.evalex; - -import java.math.BigDecimal; -import java.math.BigInteger; -import java.math.MathContext; -import java.math.RoundingMode; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Deque; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.Stack; -import java.util.TreeMap; - - -/** - *

EvalEx - Java Expression Evaluator

- * - *

Introduction

EvalEx is a handy expression evaluator for Java, that - * allows to evaluate simple mathematical and boolean expressions.
For more information, see: - * EvalEx GitHub - * repository - * - *

- * Thanks to all who contributed to this project: Contributors - * - * @see GitHub repository - */ -public class Expression { - - /** - * Unary operators precedence: + and - as prefix - */ - public static final int OPERATOR_PRECEDENCE_UNARY = 60; - - /** - * Equality operators precedence: =, ==, !=. <> - */ - public static final int OPERATOR_PRECEDENCE_EQUALITY = 7; - - /** - * Comparative operators precedence: <,>,<=,>= - */ - public static final int OPERATOR_PRECEDENCE_COMPARISON = 10; - - /** - * Or operator precedence: || - */ - public static final int OPERATOR_PRECEDENCE_OR = 2; - - /** - * And operator precedence: && - */ - public static final int OPERATOR_PRECEDENCE_AND = 4; - - /** - * Power operator precedence: ^ - */ - public static final int OPERATOR_PRECEDENCE_POWER = 40; - - /** - * An optional higher power operator precedence. {@link ExpressionSettings} - */ - public static final int OPERATOR_PRECEDENCE_POWER_HIGHER = 80; - - /** - * Multiplicative operators precedence: *,/,% - */ - public static final int OPERATOR_PRECEDENCE_MULTIPLICATIVE = 30; - - /** - * Additive operators precedence: + and - - */ - public static final int OPERATOR_PRECEDENCE_ADDITIVE = 20; - - /** - * Definition of PI as a constant, can be used in expressions as variable. - */ - public static final BigDecimal PI = new BigDecimal( - "3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679"); - - /** - * Definition of e: "Euler's number" as a constant, can be used in expressions as variable. - */ - public static final BigDecimal e = new BigDecimal( - "2.71828182845904523536028747135266249775724709369995957496696762772407663"); - - /** - * Exception message for missing operators. - */ - public static final String MISSING_PARAMETERS_FOR_OPERATOR = "Missing parameter(s) for operator "; - - /** - * The {@link MathContext} to use for calculations. - */ - private MathContext mc; - - /** - * The precedence of the power (^) operator. Default is 40. - */ - private int powerOperatorPrecedence = OPERATOR_PRECEDENCE_POWER; - - /** - * The characters (other than letters and digits) allowed as the first character in a variable. - */ - private String firstVarChars = "_"; - - /** - * The characters (other than letters and digits) allowed as the second or subsequent characters - * in a variable. - */ - private String varChars = "_"; - - /** - * The original infix expression. - */ - private final String originalExpression; - - /** - * The current infix expression, with optional variable substitutions. - */ - private String expressionString = null; - - /** - * The cached RPN (Reverse Polish Notation) of the expression. - */ - private List rpn = null; - - /** - * All defined operators with name and implementation. - */ - protected Map operators = new TreeMap( - String.CASE_INSENSITIVE_ORDER); - - /** - * All defined functions with name and implementation. - */ - protected Map functions = new TreeMap( - String.CASE_INSENSITIVE_ORDER); - - /** - * All defined variables with name and value. - */ - protected Map variables = new TreeMap( - String.CASE_INSENSITIVE_ORDER); - - /** - * What character to use for decimal separators. - */ - private static final char DECIMAL_SEPARATOR = '.'; - - /** - * What character to use for minus sign (negative values). - */ - private static final char MINUS_SIGN = '-'; - - /** - * A map of symbols that stand for constant values. These are not considered variables by {@link #getUsedVariables()}. - */ - private static final Map DEFAULT_CONSTANTS = new HashMap(); - static { - DEFAULT_CONSTANTS.put("e", e); - DEFAULT_CONSTANTS.put("PI", PI); - DEFAULT_CONSTANTS.put("NULL", null); - DEFAULT_CONSTANTS.put("TRUE", BigDecimal.ONE); - DEFAULT_CONSTANTS.put("FALSE", BigDecimal.ZERO); - } - - /** - * The BigDecimal representation of the left parenthesis, used for parsing varying numbers of - * function parameters. - */ - private static final LazyNumber PARAMS_START = new LazyNumber() { - public BigDecimal eval() { - return null; - } - - public String getString() { - return null; - } - }; - - - /** - * The expression evaluators exception class. - */ - public static class ExpressionException extends RuntimeException { - - private static final long serialVersionUID = 1118142866870779047L; - - public ExpressionException(String message) { - super(message); - } - - public ExpressionException(String message, int characterPosition) { - super(message + " at character position " + characterPosition); - } - } - - - /** - * LazyNumber interface created for lazily evaluated functions - */ - public interface LazyNumber { - - BigDecimal eval(); - - String getString(); - } - - - /** - * Construct a LazyNumber from a BigDecimal - */ - protected LazyNumber createLazyNumber(final BigDecimal bigDecimal) { - return new LazyNumber() { - @Override - public String getString() { - return bigDecimal.toPlainString(); - } - - @Override - public BigDecimal eval() { - return bigDecimal; - } - }; - } - - - /** - * LazyFunction class - */ - public abstract class LazyFunction extends AbstractLazyFunction { - - /** - * Creates a new function with given name and parameter count. - * - * @param name The name of the function. - * @param numParams The number of parameters for this function. - * -1 denotes a variable number of parameters. - * @param booleanFunction Whether this function is a boolean function. - */ - public LazyFunction(String name, int numParams, boolean booleanFunction) { - super(name, numParams, booleanFunction); - } - - /** - * Creates a new function with given name and parameter count. - * - * @param name The name of the function. - * @param numParams The number of parameters for this function. - * -1 denotes a variable number of parameters. - */ - public LazyFunction(String name, int numParams) { - super(name, numParams); - } - } - - - /** - * Abstract definition of a supported expression function. A function is defined by a name, the - * number of parameters and the actual processing implementation. - */ - public abstract class Function extends AbstractFunction { - - public Function(String name, int numParams) { - super(name, numParams); - } - - public Function(String name, int numParams, boolean booleanFunction) { - super(name, numParams, booleanFunction); - } - } - - - /** - * Abstract definition of a supported operator. An operator is defined by its name (pattern), - * precedence, if it is left- or right-associative, if it is boolean and if it is unary (number of - * operands expected for the operation is 1 or 2). - */ - public abstract class Operator extends AbstractOperator { - - /** - * Creates a new unary operator. - * - * @param oper The operator name (pattern). - * @param precedence The operators precedence. - * @param leftAssoc true if the operator is left associative, - * else false. - * @param booleanOperator Whether this operator is boolean. - * @param unaryOperator Whether this operator is unary. - */ - public Operator(String oper, int precedence, boolean leftAssoc, boolean booleanOperator, - boolean unaryOperator) { - super(oper, precedence, leftAssoc, booleanOperator, unaryOperator); - } - - - /** - * Creates a new boolean operator. - * - * @param oper The operator name (pattern). - * @param precedence The operators precedence. - * @param leftAssoc true if the operator is left associative, - * else false. - * @param booleanOperator Whether this operator is boolean. - */ - public Operator(String oper, int precedence, boolean leftAssoc, boolean booleanOperator) { - super(oper, precedence, leftAssoc, booleanOperator); - } - - - /** - * Creates a new operator. - * - * @param oper The operator name (pattern). - * @param precedence The operators precedence. - * @param leftAssoc true if the operator is left associative, - * else false. - */ - public Operator(String oper, int precedence, boolean leftAssoc) { - super(oper, precedence, leftAssoc); - } - } - - - /** - * Abstract definition of a prefix unary operator with right-side operand. It is defined by its - * name (pattern), precedence and if it is left- or right-associative. For this class, the - * operator is expected to have the single operand to its right. - */ - public abstract class UnaryOperator extends AbstractUnaryOperator { - - public UnaryOperator(String oper, int precedence, boolean leftAssoc) { - super(oper, precedence, leftAssoc); - } - } - - - enum TokenType { - VARIABLE, FUNCTION, LITERAL, OPERATOR, UNARY_OPERATOR, OPEN_PAREN, COMMA, CLOSE_PAREN, HEX_LITERAL, STRINGPARAM - } - - /** - * Token class - */ - public class Token { - - public String surface = ""; - public TokenType type; - public int pos; - - public void append(char c) { - surface += c; - } - - public void append(String s) { - surface += s; - } - - public char charAt(int pos) { - return surface.charAt(pos); - } - - public int length() { - return surface.length(); - } - - @Override - public String toString() { - return surface; - } - } - - - /** - * Expression tokenizer that allows to iterate over a {@link String} expression token by token. - * Blank characters will be skipped. - */ - private class Tokenizer implements Iterator { - - /** - * Actual position in expression string. - */ - private int pos = 0; - - /** - * The original input expression. - */ - private String input; - /** - * The previous token or null if none. - */ - private Token previousToken; - - /** - * The next token or null if none. - */ - private Token nextToken = new Token(); - - /** - * Creates a new tokenizer for an expression. - * - * @param input The expression string. - */ - public Tokenizer(String input) { - this.input = input.trim(); - } - - @Override - public boolean hasNext() { - return (pos < input.length()); - } - - /** - * Peek at the next character, without advancing the iterator. - * - * @return The next character or character 0, if at end of string. - */ - private char peekNextChar() { - return pos >= input.length()-1 ? 0 : input.charAt(pos+1); - } - - /** - * Append the current character to the given token, and advance the tokenizer position. - * - * @param token the destination token for the character - * @return The next character or character 0, if at end of string. - */ - private char consumeChar(Token token) { - token.append(input.charAt(pos++)); - return pos >= input.length() ? 0 : input.charAt(pos); - } - - /** - * Discard the current character, and advance the tokenizer position by 1. - * - * @return The next character or character 0, if at end of string. - */ - private char skipChar() { - pos++; - return pos >= input.length() ? 0 : input.charAt(pos); - } - - private boolean isHexDigit(char ch) { - return ch == 'x' || ch == 'X' || (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') - || (ch >= 'A' && ch <= 'F'); - } - - @Override - public Token next() { - Token token = new Token(); - - if (pos >= input.length()) { - previousToken = null; - return null; - } - char ch = input.charAt(pos); - while (Character.isWhitespace(ch) && pos < input.length()) { - ch = skipChar(); - } - token.pos = pos; - - if (pos < input.length() - 1) { - nextToken.pos = pos + 1; - } else { - nextToken = null; - } - - boolean isHex = false; - - if (Character.isDigit(ch) || (ch == DECIMAL_SEPARATOR && Character.isDigit(peekNextChar()))) { - if (ch == '0' && (peekNextChar() == 'x' || peekNextChar() == 'X')) { - isHex = true; - } - while ((isHex - && isHexDigit( - ch)) - || (Character.isDigit(ch) || ch == DECIMAL_SEPARATOR || ch == 'e' || ch == 'E' - || (ch == MINUS_SIGN && token.length() > 0 - && ('e' == token.charAt(token.length() - 1) - || 'E' == token.charAt(token.length() - 1))) - || (ch == '+' && token.length() > 0 - && ('e' == token.charAt(token.length() - 1) - || 'E' == token.charAt(token.length() - 1)))) - && (pos < input.length())) { - ch = consumeChar(token); - } - token.type = isHex ? TokenType.HEX_LITERAL : TokenType.LITERAL; - } else if (ch == '"') { - tokenizeString(token); - } else if (Character.isLetter(ch) || firstVarChars.indexOf(ch) >= 0) { - while ((Character.isLetter(ch) || Character.isDigit(ch) || varChars.indexOf(ch) >= 0 - || token.length() == 0 && firstVarChars.indexOf(ch) >= 0) && (pos < input.length())) { - ch = consumeChar(token); - } - // Remove optional white spaces after function or variable name - if (Character.isWhitespace(ch)) { - while (Character.isWhitespace(ch) && pos < input.length()) { - ch = skipChar(); - } - pos--; - } - if (operators.containsKey(token.surface)) { - token.type = TokenType.OPERATOR; - } else if (ch == '(') { - token.type = TokenType.FUNCTION; - } else { - token.type = TokenType.VARIABLE; - } - } else if (ch == '(' || ch == ')' || ch == ',') { - if (ch == '(') { - token.type = TokenType.OPEN_PAREN; - } else if (ch == ')') { - token.type = TokenType.CLOSE_PAREN; - } else { - token.type = TokenType.COMMA; - } - consumeChar(token); - } else { - String greedyMatch = ""; - int initialPos = pos; - ch = input.charAt(pos); - int validOperatorSeenUntil = -1; - while (!Character.isLetter(ch) && !Character.isDigit(ch) && firstVarChars.indexOf(ch) < 0 - && !Character.isWhitespace(ch) && ch != '(' && ch != ')' && ch != ',' - && (pos < input.length())) { - greedyMatch += ch; - ch = skipChar(); - if (operators.containsKey(greedyMatch)) { - validOperatorSeenUntil = pos; - } - } - if (validOperatorSeenUntil != -1) { - token.append(input.substring(initialPos, validOperatorSeenUntil)); - pos = validOperatorSeenUntil; - } else { - token.append(greedyMatch); - } - /* If the previousToken is of TokenType.OPERATOR, its parameter unaryOperator must be false to - * accept TokenType.UNARY_OPERATOR for the current token - */ - if (previousToken == null || (previousToken.type == TokenType.OPERATOR && !operators - .get(previousToken.surface).isUnaryOperator()) - || previousToken.type == TokenType.OPEN_PAREN || previousToken.type == TokenType.COMMA - || previousToken.type == TokenType.UNARY_OPERATOR) { - token.surface += "u"; - token.type = TokenType.UNARY_OPERATOR; - } else { - token.type = TokenType.OPERATOR; - } - } - previousToken = token; - return token; - } - - private void tokenizeString(Token token) { - char ch; - // skip opening quote - ch = skipChar(); - - // consume string contents - while (ch != '"' && ch != 0) { - ch = consumeChar(token); - } - - // deal with the closing quote - if (ch == 0) { - throw new TokenizerException("unterminated string literal", token.pos); - } - skipChar(); - - token.type = TokenType.STRINGPARAM; - } - - @Override - public void remove() { - throw new ExpressionException("remove() not supported"); - } - - } - - - /** - * Creates a new expression instance from an expression string with a given default match context - * of {@link MathContext#DECIMAL32}. - * - * @param expression The expression. E.g. "2.4*sin(3)/(2-4)" or - * "sin(y)>0 & max(z, 3)>3" - */ - public Expression(String expression) { - this(expression, MathContext.DECIMAL32); - } - - - /** - * Creates a new expression instance from an expression string with a given default match - * context. - * - * @param expression The expression. E.g. "2.4*sin(3)/(2-4)" or - * "sin(y)>0 & max(z, 3)>3" - * @param defaultMathContext The {@link MathContext} to use by default. - */ - public Expression(String expression, MathContext defaultMathContext) { - this(expression, ExpressionSettings - .builder() - .mathContext(defaultMathContext) - .build()); - } - - /** - * Creates a new expression instance from an expression string with given settings. - * - * @param expression The expression. E.g. "2.4*sin(3)/(2-4)" or - * "sin(y)>0 & max(z, 3)>3" - * @param expressionSettings The {@link ExpressionSettings} to use by default. - */ - public Expression(String expression, - ExpressionSettings expressionSettings) { // NOSONAR- cognitive complexity - this.mc = expressionSettings.getMathContext(); - this.powerOperatorPrecedence = expressionSettings.getPowerOperatorPrecedence(); - this.expressionString = expression; - this.originalExpression = expression; - addOperator(new Operator("+", OPERATOR_PRECEDENCE_ADDITIVE, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - assertNotNull(v1, v2); - return v1.add(v2, mc); - } - }); - addOperator(new Operator("-", OPERATOR_PRECEDENCE_ADDITIVE, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - assertNotNull(v1, v2); - return v1.subtract(v2, mc); - } - }); - addOperator(new Operator("*", OPERATOR_PRECEDENCE_MULTIPLICATIVE, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - assertNotNull(v1, v2); - return v1.multiply(v2, mc); - } - }); - addOperator(new Operator("/", OPERATOR_PRECEDENCE_MULTIPLICATIVE, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - assertNotNull(v1, v2); - return v1.divide(v2, mc); - } - }); - addOperator(new Operator("%", OPERATOR_PRECEDENCE_MULTIPLICATIVE, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - assertNotNull(v1, v2); - return v1.remainder(v2, mc); - } - }); - addOperator(new Operator("^", powerOperatorPrecedence, false) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - assertNotNull(v1, v2); - /*- - * Thanks to Gene Marin: - * http://stackoverflow.com/questions/3579779/how-to-do-a-fractional-power-on-bigdecimal-in-java - */ - int signOf2 = v2.signum(); - double dn1 = v1.doubleValue(); - v2 = v2.multiply(new BigDecimal(signOf2)); // n2 is now positive - BigDecimal remainderOf2 = v2.remainder(BigDecimal.ONE); - BigDecimal n2IntPart = v2.subtract(remainderOf2); - BigDecimal intPow = v1.pow(n2IntPart.intValueExact(), mc); - BigDecimal doublePow = BigDecimal.valueOf(Math.pow(dn1, remainderOf2.doubleValue())); - - BigDecimal result = intPow.multiply(doublePow, mc); - if (signOf2 == -1) { - result = BigDecimal.ONE.divide(result, mc.getPrecision(), RoundingMode.HALF_UP); - } - return result; - } - }); - addOperator(new Operator("&&", OPERATOR_PRECEDENCE_AND, false, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - assertNotNull(v1, v2); - - boolean b1 = v1.compareTo(BigDecimal.ZERO) != 0; - - if (!b1) { - return BigDecimal.ZERO; - } - - boolean b2 = v2.compareTo(BigDecimal.ZERO) != 0; - return b2 ? BigDecimal.ONE : BigDecimal.ZERO; - } - }); - - addOperator(new Operator("||", OPERATOR_PRECEDENCE_OR, false, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - assertNotNull(v1, v2); - - boolean b1 = v1.compareTo(BigDecimal.ZERO) != 0; - - if (b1) { - return BigDecimal.ONE; - } - - boolean b2 = v2.compareTo(BigDecimal.ZERO) != 0; - return b2 ? BigDecimal.ONE : BigDecimal.ZERO; - } - }); - - addOperator(new Operator(">", OPERATOR_PRECEDENCE_COMPARISON, false, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - assertNotNull(v1, v2); - return v1.compareTo(v2) == 1 ? BigDecimal.ONE : BigDecimal.ZERO; - } - }); - - addOperator(new Operator(">=", OPERATOR_PRECEDENCE_COMPARISON, false, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - assertNotNull(v1, v2); - return v1.compareTo(v2) >= 0 ? BigDecimal.ONE : BigDecimal.ZERO; - } - }); - - addOperator(new Operator("<", OPERATOR_PRECEDENCE_COMPARISON, false, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - assertNotNull(v1, v2); - return v1.compareTo(v2) == -1 ? BigDecimal.ONE : BigDecimal.ZERO; - } - }); - - addOperator(new Operator("<=", OPERATOR_PRECEDENCE_COMPARISON, false, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - assertNotNull(v1, v2); - return v1.compareTo(v2) <= 0 ? BigDecimal.ONE : BigDecimal.ZERO; - } - }); - - addOperator(new Operator("=", OPERATOR_PRECEDENCE_EQUALITY, false, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - if (v1 == v2) { - return BigDecimal.ONE; - } - if (v1 == null || v2 == null) { - return BigDecimal.ZERO; - } - return v1.compareTo(v2) == 0 ? BigDecimal.ONE : BigDecimal.ZERO; - } - }); - addOperator(new Operator("==", OPERATOR_PRECEDENCE_EQUALITY, false, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - return ((Operator) operators.get("=")).eval(v1, v2); - } - }); - - addOperator(new Operator("!=", OPERATOR_PRECEDENCE_EQUALITY, false, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - if (v1 == v2) { - return BigDecimal.ZERO; - } - if (v1 == null || v2 == null) { - return BigDecimal.ONE; - } - return v1.compareTo(v2) != 0 ? BigDecimal.ONE : BigDecimal.ZERO; - } - }); - addOperator(new Operator("<>", OPERATOR_PRECEDENCE_EQUALITY, false, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - assertNotNull(v1, v2); - return ((Operator) operators.get("!=")).eval(v1, v2); - } - }); - - addOperator(new UnaryOperator("-", OPERATOR_PRECEDENCE_UNARY, false) { - @Override - public BigDecimal evalUnary(BigDecimal v1) { - return v1.multiply(new BigDecimal(-1)); - } - }); - addOperator(new UnaryOperator("+", OPERATOR_PRECEDENCE_UNARY, false) { - @Override - public BigDecimal evalUnary(BigDecimal v1) { - return v1.multiply(BigDecimal.ONE); - } - }); - - addFunction(new Function("FACT", 1, false) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - - int number = parameters.get(0).intValue(); - BigDecimal factorial = BigDecimal.ONE; - for (int i = 1; i <= number; i++) { - factorial = factorial.multiply(new BigDecimal(i)); - } - return factorial; - } - }); - - addFunction(new Function("NOT", 1, true) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - boolean zero = parameters.get(0).compareTo(BigDecimal.ZERO) == 0; - return zero ? BigDecimal.ONE : BigDecimal.ZERO; - } - }); - - addLazyFunction(new LazyFunction("IF", 3) { - @Override - public LazyNumber lazyEval(List lazyParams) { - return new LazyIfNumber(lazyParams); - } - }); - - addFunction(new Function("RANDOM", 0) { - @Override - public BigDecimal eval(List parameters) { - double d = Math.random(); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("SINR", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.sin(parameters.get(0).doubleValue()); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("COSR", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.cos(parameters.get(0).doubleValue()); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("TANR", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.tan(parameters.get(0).doubleValue()); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("COTR", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - /* Formula: cot(x) = cos(x) / sin(x) = 1 / tan(x) */ - double one = 1; - double d = Math.tan(parameters.get(0).doubleValue()); - return new BigDecimal((one / d), mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("SECR", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - /* Formula: sec(x) = 1 / cos(x) */ - double one = 1; - double d = Math.cos(parameters.get(0).doubleValue()); - return new BigDecimal((one / d), mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("CSCR", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - /* Formula: csc(x) = 1 / sin(x) */ - double one = 1; - double d = Math.sin(parameters.get(0).doubleValue()); - return new BigDecimal((one / d), mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("SIN", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.sin(Math.toRadians(parameters.get(0).doubleValue())); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("COS", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.cos(Math.toRadians(parameters.get(0).doubleValue())); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("TAN", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.tan(Math.toRadians(parameters.get(0).doubleValue())); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("COT", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - /* Formula: cot(x) = cos(x) / sin(x) = 1 / tan(x) */ - double one = 1; - double d = Math.tan(Math.toRadians(parameters.get(0).doubleValue())); - return new BigDecimal((one / d), mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("SEC", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - /* Formula: sec(x) = 1 / cos(x) */ - double one = 1; - double d = Math.cos(Math.toRadians(parameters.get(0).doubleValue())); - return new BigDecimal((one / d), mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("CSC", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - /* Formula: csc(x) = 1 / sin(x) */ - double one = 1; - double d = Math.sin(Math.toRadians(parameters.get(0).doubleValue())); - return new BigDecimal((one / d), mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("ASINR", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.asin(parameters.get(0).doubleValue()); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("ACOSR", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.acos(parameters.get(0).doubleValue()); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("ATANR", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.atan(parameters.get(0).doubleValue()); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("ACOTR", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - /* Formula: acot(x) = atan(1/x) */ - if (parameters.get(0).doubleValue() == 0) { - throw new ExpressionException("Number must not be 0"); - } - double d = Math.atan(1 / parameters.get(0).doubleValue()); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("ATAN2R", 2) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0), parameters.get(1)); - double d = Math.atan2(parameters.get(0).doubleValue(), parameters.get(1).doubleValue()); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("ASIN", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.toDegrees(Math.asin(parameters.get(0).doubleValue())); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("ACOS", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.toDegrees(Math.acos(parameters.get(0).doubleValue())); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("ATAN", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.toDegrees(Math.atan(parameters.get(0).doubleValue())); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("ACOT", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - /* Formula: acot(x) = atan(1/x) */ - if (parameters.get(0).doubleValue() == 0) { - throw new ExpressionException("Number must not be 0"); - } - double d = Math.toDegrees(Math.atan(1 / parameters.get(0).doubleValue())); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("ATAN2", 2) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0), parameters.get(1)); - double d = Math.toDegrees( - Math.atan2(parameters.get(0).doubleValue(), parameters.get(1).doubleValue())); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("SINH", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.sinh(parameters.get(0).doubleValue()); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("COSH", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.cosh(parameters.get(0).doubleValue()); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("TANH", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.tanh(parameters.get(0).doubleValue()); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("SECH", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - /* Formula: sech(x) = 1 / cosh(x) */ - double one = 1; - double d = Math.cosh(parameters.get(0).doubleValue()); - return new BigDecimal((one / d), mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("CSCH", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - /* Formula: csch(x) = 1 / sinh(x) */ - double one = 1; - double d = Math.sinh(parameters.get(0).doubleValue()); - return new BigDecimal((one / d), mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("COTH", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - /* Formula: coth(x) = 1 / tanh(x) */ - double one = 1; - double d = Math.tanh(parameters.get(0).doubleValue()); - return new BigDecimal((one / d), mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("ASINH", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - /* Formula: asinh(x) = ln(x + sqrt(x^2 + 1)) */ - double d = Math.log(parameters.get(0).doubleValue() - + (Math.sqrt(Math.pow(parameters.get(0).doubleValue(), 2) + 1))); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("ACOSH", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - /* Formula: acosh(x) = ln(x + sqrt(x^2 - 1)) */ - if (Double.compare(parameters.get(0).doubleValue(), 1) < 0) { - throw new ExpressionException("Number must be x >= 1"); - } - double d = Math.log(parameters.get(0).doubleValue() - + (Math.sqrt(Math.pow(parameters.get(0).doubleValue(), 2) - 1))); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("ATANH", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - /* Formula: atanh(x) = 0.5*ln((1 + x)/(1 - x)) */ - if (Math.abs(parameters.get(0).doubleValue()) > 1 - || Math.abs(parameters.get(0).doubleValue()) == 1) { - throw new ExpressionException("Number must be |x| < 1"); - } - double d = 0.5 - * Math - .log((1 + parameters.get(0).doubleValue()) / (1 - parameters.get(0).doubleValue())); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("RAD", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.toRadians(parameters.get(0).doubleValue()); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("DEG", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.toDegrees(parameters.get(0).doubleValue()); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("MAX", -1) { - @Override - public BigDecimal eval(List parameters) { - if (parameters.isEmpty()) { - throw new ExpressionException("MAX requires at least one parameter"); - } - BigDecimal max = null; - for (BigDecimal parameter : parameters) { - assertNotNull(parameter); - if (max == null || parameter.compareTo(max) > 0) { - max = parameter; - } - } - return max; - } - }); - addFunction(new Function("MIN", -1) { - @Override - public BigDecimal eval(List parameters) { - if (parameters.isEmpty()) { - throw new ExpressionException("MIN requires at least one parameter"); - } - BigDecimal min = null; - for (BigDecimal parameter : parameters) { - assertNotNull(parameter); - if (min == null || parameter.compareTo(min) < 0) { - min = parameter; - } - } - return min; - } - }); - addFunction(new Function("ABS", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - return parameters.get(0).abs(mc); - } - }); - addFunction(new Function("LOG", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.log(parameters.get(0).doubleValue()); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("LOG10", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - double d = Math.log10(parameters.get(0).doubleValue()); - return new BigDecimal(d, mc); // NOSONAR - false positive, mc is passed - } - }); - addFunction(new Function("ROUND", 2) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0), parameters.get(1)); - BigDecimal toRound = parameters.get(0); - int precision = parameters.get(1).intValue(); - return toRound.setScale(precision, mc.getRoundingMode()); - } - }); - addFunction(new Function("FLOOR", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - BigDecimal toRound = parameters.get(0); - return toRound.setScale(0, RoundingMode.FLOOR); - } - }); - addFunction(new Function("CEILING", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - BigDecimal toRound = parameters.get(0); - return toRound.setScale(0, RoundingMode.CEILING); - } - }); - addFunction(new Function("SQRT", 1) { - @Override - public BigDecimal eval(List parameters) { - assertNotNull(parameters.get(0)); - /* - * From The Java Programmers Guide To numerical Computing - * (Ronald Mak, 2003) - */ - BigDecimal x = parameters.get(0); - if (x.compareTo(BigDecimal.ZERO) == 0) { - return new BigDecimal(0); - } - if (x.signum() < 0) { - throw new ExpressionException("Argument to SQRT() function must not be negative"); - } - BigInteger n = x.movePointRight(mc.getPrecision() << 1).toBigInteger(); - - int bits = (n.bitLength() + 1) >> 1; - BigInteger ix = n.shiftRight(bits); - BigInteger ixPrev; - BigInteger test; - do { - ixPrev = ix; - ix = ix.add(n.divide(ix)).shiftRight(1); - // Give other threads a chance to work - Thread.yield(); - test = ix.subtract(ixPrev).abs(); - } while (test.compareTo(BigInteger.ZERO) != 0 && test.compareTo(BigInteger.ONE) != 0); - - return new BigDecimal(ix, mc.getPrecision()); - } - }); - - for (Map.Entry constant : DEFAULT_CONSTANTS.entrySet()) { - BigDecimal value = constant.getValue(); - variables.put(constant.getKey(), value == null? null : createLazyNumber(value)); - } - } - - - /** - * Assert the single expected operand is not null. - */ - public static void assertNotNull(BigDecimal v1) { - if (v1 == null) { - throw new ArithmeticException("Operand may not be null"); - } - } - - - /** - * Assert the two expected operands are not null. - */ - public static void assertNotNull(BigDecimal v1, BigDecimal v2) { - if (v1 == null) { - throw new ArithmeticException("First operand may not be null"); - } - if (v2 == null) { - throw new ArithmeticException("Second operand may not be null"); - } - } - - - /** - * Is the string a number? - * - * @param st The string. - * @return true, if the input string is a number. - */ - protected boolean isNumber(String st) { - if (st.isEmpty()) { - return false; - } - if (st.charAt(0) == MINUS_SIGN && st.length() == 1) { - return false; - } - if (st.charAt(0) == '+' && st.length() == 1) { - return false; - } - if (st.charAt(0) == DECIMAL_SEPARATOR && (st.length() == 1 || !Character - .isDigit(st.charAt(1)))) { - return false; - } - if (st.charAt(0) == 'e' || st.charAt(0) == 'E') { - return false; - } - for (char ch : st.toCharArray()) { - if (!Character.isDigit(ch) && ch != MINUS_SIGN && ch != DECIMAL_SEPARATOR && ch != 'e' - && ch != 'E' - && ch != '+') { - return false; - } - } - return true; - } - - - /** - * Implementation of the Shunting Yard algorithm to transform an infix expression to a RPN - * expression. - * - * @param expression The input expression in infx. - * @return A RPN representation of the expression, with each token as a list member. - */ - @SuppressWarnings("incomplete-switch") - private List shuntingYard(String expression) { - List outputQueue = new ArrayList(); - Stack stack = new Stack(); // NOSONAR - Stack is needed here - - Tokenizer tokenizer = new Tokenizer(expression); - - Token lastFunction = null; - Token previousToken = null; - while (tokenizer.hasNext()) { - Token token = tokenizer.next(); - switch (token.type) { - case STRINGPARAM: - stack.push(token); - break; - case LITERAL: - case HEX_LITERAL: - if (previousToken != null && (previousToken.type == TokenType.LITERAL - || previousToken.type == TokenType.HEX_LITERAL)) { - throw new ExpressionException("Missing operator", token.pos); - } - outputQueue.add(token); - break; - case VARIABLE: - outputQueue.add(token); - break; - case FUNCTION: - stack.push(token); - lastFunction = token; - break; - case COMMA: - if (previousToken != null && previousToken.type == TokenType.OPERATOR) { - throw new ExpressionException(MISSING_PARAMETERS_FOR_OPERATOR + previousToken, - previousToken.pos); - } - while (!stack.isEmpty() && stack.peek().type != TokenType.OPEN_PAREN) { - outputQueue.add(stack.pop()); - } - if (stack.isEmpty()) { - if (lastFunction == null) { - throw new ExpressionException("Unexpected comma", token.pos); - } else { - throw new ExpressionException( - "Parse error for function " + lastFunction, token.pos); - } - } - break; - case OPERATOR: { - /* ExpressionException thrown only for operators which unaryOperator = false, - * allowing postfix unary operators without "MISSING_PARAMETERS_FOR_OPERATOR" exception. - */ - if (previousToken != null && operators.containsKey(token.surface) - && (previousToken.type == TokenType.COMMA - || previousToken.type == TokenType.OPEN_PAREN)) { - if (!operators.get(token.surface).isUnaryOperator()) { - throw new ExpressionException( - MISSING_PARAMETERS_FOR_OPERATOR + token, token.pos); - } - } - LazyOperator o1 = operators.get(token.surface); - if (o1 == null) { - throw new ExpressionException("Unknown operator " + token, token.pos + 1); - } - - shuntOperators(outputQueue, stack, o1); - stack.push(token); - break; - } - case UNARY_OPERATOR: { - if (previousToken != null && previousToken.type != TokenType.OPERATOR - && previousToken.type != TokenType.COMMA && previousToken.type != TokenType.OPEN_PAREN - && previousToken.type != TokenType.UNARY_OPERATOR) { - throw new ExpressionException( - "Invalid position for unary operator " + token, token.pos); - } - LazyOperator o1 = operators.get(token.surface); - if (o1 == null) { - throw new ExpressionException( - "Unknown unary operator " + token.surface.substring(0, token.surface.length() - 1) - , token.pos + 1); - } - - shuntOperators(outputQueue, stack, o1); - stack.push(token); - break; - } - case OPEN_PAREN: - if (previousToken != null) { - if (previousToken.type == TokenType.LITERAL - || previousToken.type == TokenType.CLOSE_PAREN - || previousToken.type == TokenType.VARIABLE - || previousToken.type == TokenType.HEX_LITERAL) { - // Implicit multiplication, e.g. 23(a+b) or (a+b)(a-b) - Token multiplication = new Token(); - multiplication.append("*"); - multiplication.type = TokenType.OPERATOR; - stack.push(multiplication); - } - // if the ( is preceded by a valid function, then it - // denotes the start of a parameter list - if (previousToken.type == TokenType.FUNCTION) { - outputQueue.add(token); - } - } - stack.push(token); - break; - case CLOSE_PAREN: - /* Handling "MISSING_PARAMETERS_FOR_OPERATOR" for postfix unary operators followed by closed parentheses. - */ - if (previousToken != null && previousToken.type == TokenType.OPERATOR && !operators - .get(previousToken.surface).isUnaryOperator()) { - throw new ExpressionException(MISSING_PARAMETERS_FOR_OPERATOR + previousToken, - previousToken.pos); - } - while (!stack.isEmpty() && stack.peek().type != TokenType.OPEN_PAREN) { - outputQueue.add(stack.pop()); - } - if (stack.isEmpty()) { - throw new ExpressionException("Mismatched parentheses"); - } - stack.pop(); - if (!stack.isEmpty() && stack.peek().type == TokenType.FUNCTION) { - outputQueue.add(stack.pop()); - } - } - previousToken = token; - } - - while (!stack.isEmpty()) { - Token element = stack.pop(); - if (element.type == TokenType.OPEN_PAREN || element.type == TokenType.CLOSE_PAREN) { - throw new ExpressionException("Mismatched parentheses"); - } - outputQueue.add(element); - } - return outputQueue; - } - - - private void shuntOperators(List outputQueue, Stack stack, - LazyOperator o1) { // NOSONAR - Stack is needed here - Expression.Token nextToken = stack.isEmpty() ? null : stack.peek(); - while (nextToken != null - && (nextToken.type == Expression.TokenType.OPERATOR - || nextToken.type == Expression.TokenType.UNARY_OPERATOR) - && ( - (o1.isLeftAssoc() && o1.getPrecedence() <= operators.get(nextToken.surface).getPrecedence()) - || (o1.getPrecedence() < operators.get(nextToken.surface).getPrecedence()))) { - outputQueue.add(stack.pop()); - nextToken = stack.isEmpty() ? null : stack.peek(); - } - } - - - /** - * Evaluates the expression. - * - * @return The result of the expression. Trailing zeros are stripped. - */ - public BigDecimal eval() { - return eval(true); - } - - - /** - * Evaluates the expression. - * - * @param stripTrailingZeros If set to true trailing zeros in the result are - * stripped. - * @return The result of the expression. - */ - public BigDecimal eval(boolean stripTrailingZeros) { - - Deque stack = new ArrayDeque(); - - for (final Token token : getRPN()) { - switch (token.type) { - case UNARY_OPERATOR: { - final LazyNumber value = stack.pop(); - LazyNumber result = new LazyNumber() { - public BigDecimal eval() { - return operators.get(token.surface).eval(value, null).eval(); - } - - @Override - public String getString() { - return String.valueOf(operators.get(token.surface).eval(value, null).eval()); - } - }; - stack.push(result); - break; - } - case OPERATOR: - /* IF-ELSE block to handle operators with 1 or 2 operands, allowing for the - * second operand v2 = null in operators which unaryOperator = true. - */ - if (operators.get(token.surface).isUnaryOperator()) { - final LazyNumber value = stack.pop(); - LazyNumber result = new LazyNumber() { - public BigDecimal eval() { - return operators.get(token.surface).eval(value, null).eval(); - } - - @Override - public String getString() { - return String.valueOf(operators.get(token.surface).eval(value, null).eval()); - } - }; - stack.push(result); - break; - } else { - final LazyNumber v1 = stack.pop(); - final LazyNumber v2 = stack.pop(); - LazyNumber result = new LazyNumber() { - public BigDecimal eval() { - return operators.get(token.surface).eval(v2, v1).eval(); - } - - public String getString() { - return String.valueOf(operators.get(token.surface).eval(v2, v1).eval()); - } - }; - stack.push(result); - break; - } - case VARIABLE: - if (!variables.containsKey(token.surface)) { - throw new ExpressionException("Unknown operator or function: " + token); - } - - stack.push(new LazyNumber() { - public BigDecimal eval() { - LazyNumber lazyVariable = variables.get(token.surface); - BigDecimal value = lazyVariable == null ? null : lazyVariable.eval(); - return value == null ? null : value.round(mc); - } - - public String getString() { - LazyNumber lazyVariable = variables.get(token.surface); - return lazyVariable.getString(); - } - }); - break; - case FUNCTION: - com.udojava.evalex.LazyFunction f = functions.get(token.surface.toUpperCase(Locale.ROOT)); - ArrayList p = new ArrayList( - !f.numParamsVaries() ? f.getNumParams() : 0); - // pop parameters off the stack until we hit the start of - // this function's parameter list - while (!stack.isEmpty() && stack.peek() != PARAMS_START) { - p.add(0, stack.pop()); - } - - if (stack.peek() == PARAMS_START) { - stack.pop(); - } - - LazyNumber fResult = f.lazyEval(p); - stack.push(fResult); - break; - case OPEN_PAREN: - stack.push(PARAMS_START); - break; - case LITERAL: - stack.push(new LazyNumber() { - public BigDecimal eval() { - if (token.surface.equalsIgnoreCase("NULL")) { - return null; - } - - return new BigDecimal(token.surface, mc); - } - - public String getString() { - return String.valueOf(new BigDecimal(token.surface, mc)); - } - }); - break; - case STRINGPARAM: - stack.push(new LazyNumber() { - public BigDecimal eval() { - return null; - } - - public String getString() { - return token.surface; - } - }); - break; - case HEX_LITERAL: - stack.push(new LazyNumber() { - public BigDecimal eval() { - return new BigDecimal(new BigInteger(token.surface.substring(2), 16), mc); - } - - public String getString() { - return new BigInteger(token.surface.substring(2), 16).toString(); - } - }); - break; - default: - throw new ExpressionException( - "Unexpected token " + token.surface, token.pos); - } - } - BigDecimal result = stack.pop().eval(); - if (result == null) { - return null; - } - if (stripTrailingZeros) { - result = result.stripTrailingZeros(); - } - return result; - } - - - /** - * Sets the precision for expression evaluation. - * - * @param precision The new precision. - * @return The expression, allows to chain methods. - */ - public Expression setPrecision(int precision) { - this.mc = new MathContext(precision); - return this; - } - - - /** - * Sets the rounding mode for expression evaluation. - * - * @param roundingMode The new rounding mode. - * @return The expression, allows to chain methods. - */ - public Expression setRoundingMode(RoundingMode roundingMode) { - this.mc = new MathContext(mc.getPrecision(), roundingMode); - return this; - } - - - /** - * Sets the characters other than letters and digits that are valid as the first character of a - * variable. - * - * @param chars The new set of variable characters. - * @return The expression, allows to chain methods. - */ - public Expression setFirstVariableCharacters(String chars) { - this.firstVarChars = chars; - return this; - } - - - /** - * Sets the characters other than letters and digits that are valid as the second and subsequent - * characters of a variable. - * - * @param chars The new set of variable characters. - * @return The expression, allows to chain methods. - */ - public Expression setVariableCharacters(String chars) { - this.varChars = chars; - return this; - } - - - /** - * Adds an operator to the list of supported operators. - * - * @param operator The operator to add. - * @return The previous operator with that name, or null if there was none. - */ - @SuppressWarnings("unchecked") - public OPERATOR addOperator(OPERATOR operator) { - String key = operator.getOper(); - if (operator instanceof AbstractUnaryOperator) { - key += "u"; - } - return (OPERATOR) operators.put(key, operator); - } - - - /** - * Adds a function to the list of supported functions - * - * @param function The function to add. - * @return The previous operator with that name, or null if there was none. - */ - public com.udojava.evalex.Function addFunction(com.udojava.evalex.Function function) { - return (com.udojava.evalex.Function) functions.put(function.getName(), function); - } - - - /** - * Adds a lazy function function to the list of supported functions - * - * @param function The function to add. - * @return The previous operator with that name, or null if there was none. - */ - public com.udojava.evalex.LazyFunction addLazyFunction(com.udojava.evalex.LazyFunction function) { - return functions.put(function.getName(), function); - } - - - /** - * Sets a variable value. - * - * @param variable The variable name. - * @param value The variable value. - * @return The expression, allows to chain methods. - */ - public Expression setVariable(String variable, BigDecimal value) { - return setVariable(variable, createLazyNumber(value)); - } - - - /** - * Sets a variable value. - * - * @param variable The variable name. - * @param value The variable value. - * @return The expression, allows to chain methods. - */ - public Expression setVariable(String variable, LazyNumber value) { - variables.put(variable, value); - return this; - } - - - /** - * Sets a variable value. - * - * @param variable The variable to set. - * @param value The variable value. - * @return The expression, allows to chain methods. - */ - public Expression setVariable(String variable, String value) { - if (isNumber(value)) { - variables.put(variable, createLazyNumber(new BigDecimal(value, mc))); - } else if (value.equalsIgnoreCase("null")) { - variables.put(variable, null); - } else { - final String expStr = value; - variables.put(variable, new LazyNumber() { - private final Map outerVariables = variables; - private final Map outerFunctions = functions; - private final Map outerOperators = operators; - private final String innerExpressionString = expStr; - private final MathContext inneMc = mc; - - @Override - public String getString() { - return innerExpressionString; - } - - @Override - public BigDecimal eval() { - Expression innerE = new Expression(innerExpressionString, inneMc); - innerE.variables = outerVariables; - innerE.functions = outerFunctions; - innerE.operators = outerOperators; - return innerE.eval(); - } - }); - rpn = null; - } - return this; - } - - - /** - * Creates a new inner expression for nested expression. - * - * @param expression The string expression. - * @return The inner Expression instance. - */ - private Expression createEmbeddedExpression(final String expression) { - final Map outerVariables = variables; - final Map outerFunctions = functions; - final Map outerOperators = operators; - final MathContext inneMc = mc; - Expression exp = new Expression(expression, inneMc); - exp.variables = outerVariables; - exp.functions = outerFunctions; - exp.operators = outerOperators; - return exp; - } - - - /** - * Sets a variable value. - * - * @param variable The variable to set. - * @param value The variable value. - * @return The expression, allows to chain methods. - */ - public Expression with(String variable, BigDecimal value) { - return setVariable(variable, value); - } - - - /** - * Sets a variable value. - * - * @param variable The variable to set. - * @param value The variable value. - * @return The expression, allows to chain methods. - */ - public Expression with(String variable, LazyNumber value) { - return setVariable(variable, value); - } - - - /** - * Sets a variable value. - * - * @param variable The variable to set. - * @param value The variable value. - * @return The expression, allows to chain methods. - */ - public Expression and(String variable, String value) { - return setVariable(variable, value); - } - - - /** - * Sets a variable value. - * - * @param variable The variable to set. - * @param value The variable value. - * @return The expression, allows to chain methods. - */ - public Expression and(String variable, BigDecimal value) { - return setVariable(variable, value); - } - - - /** - * Sets a variable value. - * - * @param variable The variable to set. - * @param value The variable value. - * @return The expression, allows to chain methods. - */ - public Expression and(String variable, LazyNumber value) { - return setVariable(variable, value); - } - - - /** - * Sets a variable value. - * - * @param variable The variable to set. - * @param value The variable value. - * @return The expression, allows to chain methods. - */ - public Expression with(String variable, String value) { - return setVariable(variable, value); - } - - - /** - * Get an iterator for this expression, allows iterating over an expression token by token. - * - * @return A new iterator instance for this expression. - */ - public Iterator getExpressionTokenizer() { - final String expression = this.expressionString; - - return new Tokenizer(expression); - } - - - /** - * Cached access to the RPN notation of this expression, ensures only one calculation of the RPN - * per expression instance. If no cached instance exists, a new one will be created and put to the - * cache. - * - * @return The cached RPN instance. - */ - private List getRPN() { - if (rpn == null) { - rpn = shuntingYard(this.expressionString); - validate(rpn); - } - return rpn; - } - - - /** - * Check that the expression has enough numbers and variables to fit the requirements of the - * operators and functions, also check for only 1 result stored at the end of the evaluation. - */ - private void validate(List rpn) { - /*- - * Thanks to Norman Ramsey: - * http://http://stackoverflow.com/questions/789847/postfix-notation-validation - */ - // each push on to this stack is a new function scope, with the value of - // each - // layer on the stack being the count of the number of parameters in - // that scope - Stack stack = new Stack(); // NOSONAR - we need a stack here for the Stack.set() method. - - // push the 'global' scope - stack.push(0); - - for (final Token token : rpn) { - switch (token.type) { - case UNARY_OPERATOR: - if (stack.peek() < 1) { - throw new ExpressionException(MISSING_PARAMETERS_FOR_OPERATOR + token); - } - break; - case OPERATOR: - LazyOperator op = operators.get(token.surface); - /* "MISSING_PARAMETERS_FOR_OPERATOR" exception only if the stack.peek() is less - * than the expected number of operands for this operator. - */ - int numberOperands = 2; - if (op.isUnaryOperator()) { - numberOperands = 1; - } - if (stack.peek() < numberOperands) { - throw new ExpressionException(MISSING_PARAMETERS_FOR_OPERATOR + token); - } - /* IF condition to modify stack popping 2 parameters and adding to result - * only for operators whose numberOperands > 1. - */ - if (numberOperands > 1) { - stack.set(stack.size() - 1, stack.peek() - numberOperands + 1); - } - break; - case FUNCTION: - com.udojava.evalex.LazyFunction f = functions.get(token.surface.toUpperCase(Locale.ROOT)); - if (f == null) { - throw new ExpressionException("Unknown function " + token, token.pos + 1); - } - - int numParams = stack.pop(); - if (!f.numParamsVaries() && numParams != f.getNumParams()) { - throw new ExpressionException( - "Function " + token + " expected " + f.getNumParams() + " parameters, got " - + numParams); - } - if (stack.isEmpty()) { - throw new ExpressionException("Too many function calls, maximum scope exceeded"); - } - // push the result of the function - stack.set(stack.size() - 1, stack.peek() + 1); - break; - case OPEN_PAREN: - stack.push(0); - break; - default: - stack.set(stack.size() - 1, stack.peek() + 1); - } - } - - if (stack.size() > 1) { - throw new ExpressionException("Too many unhandled function parameter lists"); - } else if (stack.peek() > 1) { - throw new ExpressionException("Too many numbers or variables"); - } else if (stack.peek() < 1) { - throw new ExpressionException("Empty expression"); - } - } - - - /** - * Get a string representation of the RPN (Reverse Polish Notation) for this expression. - * - * @return A string with the RPN representation for this expression. - */ - public String toRPN() { - StringBuilder result = new StringBuilder(); - for (Token t : getRPN()) { - if (result.length() != 0) { - result.append(" "); - } - if (t.type == TokenType.VARIABLE && variables.containsKey(t.surface)) { - LazyNumber innerVariable = variables.get(t.surface); - String innerExp = innerVariable.getString(); - if (isNumber(innerExp)) { // if it is a number, then we don't - // expan in the RPN - result.append(t.toString()); - } else { // expand the nested variable to its RPN representation - Expression exp = createEmbeddedExpression(innerExp); - String nestedExpRpn = exp.toRPN(); - result.append(nestedExpRpn); - } - } else { - result.append(t.toString()); - } - } - return result.toString(); - } - - - /** - * Exposing declared variables in the expression. - * - * @return All declared variables. - */ - public Set getDeclaredVariables() { - return Collections.unmodifiableSet(variables.keySet()); - } - - - /** - * Exposing declared operators in the expression. - * - * @return All declared operators. - */ - public Set getDeclaredOperators() { - return Collections.unmodifiableSet(operators.keySet()); - } - - - /** - * Exposing declared functions. - * - * @return All declared functions. - */ - public Set getDeclaredFunctions() { - return Collections.unmodifiableSet(functions.keySet()); - } - - - /** - * @return The original expression string - */ - public String getExpression() { - return expressionString; - } - - - /** - * Returns a list of the variables in the expression. - * - * @return A list of the variable names in this expression. - */ - public List getUsedVariables() { - List result = new ArrayList(); - Tokenizer tokenizer = new Tokenizer(expressionString); - while (tokenizer.hasNext()) { - Token nextToken = tokenizer.next(); - String token = nextToken.toString(); - if (nextToken.type != TokenType.VARIABLE || DEFAULT_CONSTANTS.containsKey(token)) { - continue; - } - result.add(token); - } - return result; - } - - - /** - * The original expression used to construct this expression, without variables substituted. - */ - public String getOriginalExpression() { - return this.originalExpression; - } - - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Expression that = (Expression) o; - if (this.expressionString == null) { - return that.expressionString == null; - } else { - return this.expressionString.equals(that.expressionString); - } - } - - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - return this.expressionString == null ? 0 : this.expressionString.hashCode(); - } - - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return this.expressionString; - } - - - /** - * Checks whether the expression is a boolean expression. An expression is considered a boolean - * expression, if the last operator or function is boolean. The IF function is handled special. If - * the third parameter is boolean, then the IF is also considered boolean, else non-boolean. - * - * @return true if the last operator/function was a boolean. - */ - public boolean isBoolean() { - List rpnList = getRPN(); - if (!rpnList.isEmpty()) { - for (int i = rpnList.size() - 1; i >= 0; i--) { - Token t = rpnList.get(i); - /* - * The IF function is handled special. If the third parameter is - * boolean, then the IF is also considered a boolean. Just skip - * the IF function to check the second parameter. - */ - if (t.surface.equals("IF")) { - continue; - } - if (t.type == TokenType.FUNCTION) { - return functions.get(t.surface).isBooleanFunction(); - } else if (t.type == TokenType.OPERATOR) { - return operators.get(t.surface).isBooleanOperator(); - } - } - } - return false; - } - - - public List infixNotation() { - final List infix = new ArrayList(); - Tokenizer tokenizer = new Tokenizer(expressionString); - while (tokenizer.hasNext()) { - Token token = tokenizer.next(); - String infixNotation = "{" + token.type + ":" + token.surface + "}"; - infix.add(infixNotation); - } - return infix; - } - -} diff --git a/src/main/java/com/udojava/evalex/ExpressionSettings.java b/src/main/java/com/udojava/evalex/ExpressionSettings.java deleted file mode 100644 index cfee139..0000000 --- a/src/main/java/com/udojava/evalex/ExpressionSettings.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.udojava.evalex; - -import java.math.MathContext; - -/** - * Expression settings can be used to set certain defaults, when creating a new expression. Settings - * are read only and can be created using a {@link ExpressionSettings#builder()}. - * - * @see Expression#Expression(String, ExpressionSettings) - */ -public class ExpressionSettings { - - /** - * The math context to use. Default is {@link MathContext#DECIMAL32}. - */ - private MathContext mathContext; - - /** - * The precedence of the power (^) operator. Default is 40. - */ - private int powerOperatorPrecedence; - - private ExpressionSettings() { - // hide default constructor - } - - /** - * Create a new settings object. - * - * @param mathContext The default math context to use. - * @param powerOperatorPrecedence The default power of operator precedence. - */ - public ExpressionSettings(MathContext mathContext, int powerOperatorPrecedence) { - this.mathContext = mathContext; - this.powerOperatorPrecedence = powerOperatorPrecedence; - } - - /** - * Get the current math context. - * - * @return The current math context. - */ - public MathContext getMathContext() { - return mathContext; - } - - /** - * Get the current power precedence. - * - * @return The current power precedence. - */ - public int getPowerOperatorPrecedence() { - return powerOperatorPrecedence; - } - - public static Builder builder() { - return new Builder(); - } - - /** - * A builder to create a read-only {@link ExpressionSettings} instance. - */ - public static class Builder { - - private MathContext mathContext = MathContext.DECIMAL32; - - private int powerOperatorPrecedence = Expression.OPERATOR_PRECEDENCE_POWER; - - public Builder mathContext(MathContext mathContext) { - this.mathContext = mathContext; - return this; - } - - public Builder powerOperatorPrecedenceHigher() { - this.powerOperatorPrecedence = Expression.OPERATOR_PRECEDENCE_POWER_HIGHER; - return this; - } - - public Builder powerOperatorPrecedence(int powerOperatorPrecedence) { - this.powerOperatorPrecedence = powerOperatorPrecedence; - return this; - } - - public ExpressionSettings build() { - return new ExpressionSettings(mathContext, powerOperatorPrecedence); - } - } -} diff --git a/src/main/java/com/udojava/evalex/Function.java b/src/main/java/com/udojava/evalex/Function.java deleted file mode 100644 index de28981..0000000 --- a/src/main/java/com/udojava/evalex/Function.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2018 Udo Klimaschewski - * - * http://UdoJava.com/ - * http://about.me/udo.klimaschewski - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - */ -package com.udojava.evalex; - -import java.math.BigDecimal; -import java.util.List; - -/** - * Base interface which is required for all directly evaluated functions. - */ -public interface Function extends LazyFunction { - - /** - * Implementation for this function. - * - * @param parameters Parameters will be passed by the expression evaluator as a {@link List} of - * {@link BigDecimal} values. - * @return The function must return a new {@link BigDecimal} value as a computing result. - */ - BigDecimal eval(List parameters); -} diff --git a/src/main/java/com/udojava/evalex/LazyFunction.java b/src/main/java/com/udojava/evalex/LazyFunction.java deleted file mode 100644 index 2c47ae2..0000000 --- a/src/main/java/com/udojava/evalex/LazyFunction.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2018 Udo Klimaschewski - * - * http://UdoJava.com/ - * http://about.me/udo.klimaschewski - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - */ -package com.udojava.evalex; - -import com.udojava.evalex.Expression.LazyNumber; -import java.util.List; - -/** - * Base interface which is required for lazily evaluated functions. A function is defined by a name, - * a number of parameters it accepts and of course the logic for evaluating the result. - */ -public interface LazyFunction { - - /** - * Gets the name of this function.
- *
- * The name is use to invoke this function in the expression. - * - * @return The name of this function. - */ - String getName(); - - /** - * Gets the number of parameters this function accepts.
- *
- * A value of -1 denotes that this function accepts a variable number of parameters. - * - * @return The number of parameters this function accepts. - */ - int getNumParams(); - - /** - * Gets whether the number of accepted parameters varies.
- *
- * That means that the function does accept an undefined amount of parameters. - * - * @return true if the number of accepted parameters varies. - */ - boolean numParamsVaries(); - - /** - * Gets whether this function evaluates to a boolean expression. - * - * @return true if this function evaluates to a boolean - * expression. - */ - boolean isBooleanFunction(); - - /** - * Lazily evaluate this function. - * - * @param lazyParams The accepted parameters. - * @return The lazy result of this function. - */ - LazyNumber lazyEval(List lazyParams); -} diff --git a/src/main/java/com/udojava/evalex/LazyIfNumber.java b/src/main/java/com/udojava/evalex/LazyIfNumber.java deleted file mode 100644 index 7c957cd..0000000 --- a/src/main/java/com/udojava/evalex/LazyIfNumber.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2018 Udo Klimaschewski - * - * http://UdoJava.com/ - * http://about.me/udo.klimaschewski - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - */ -package com.udojava.evalex; - -import com.udojava.evalex.Expression.LazyNumber; -import java.math.BigDecimal; -import java.util.List; - -/** - * Lazy Number for IF function created for lazily evaluated IF condition - */ -public class LazyIfNumber implements LazyNumber { - - private List lazyParams; - - public LazyIfNumber(List lazyParams) { - this.lazyParams = lazyParams; - } - - @Override - public BigDecimal eval() { - BigDecimal result = lazyParams.get(0).eval(); - assertNotNull(result); - boolean isTrue = result.compareTo(BigDecimal.ZERO) != 0; - return isTrue ? lazyParams.get(1).eval() : lazyParams.get(2).eval(); - } - - @Override - public String getString() { - return lazyParams.get(0).getString(); - } - - private void assertNotNull(BigDecimal v1) { - if (v1 == null) { - throw new ArithmeticException("Operand may not be null"); - } - } -} diff --git a/src/main/java/com/udojava/evalex/LazyOperator.java b/src/main/java/com/udojava/evalex/LazyOperator.java deleted file mode 100644 index a5e05d7..0000000 --- a/src/main/java/com/udojava/evalex/LazyOperator.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2018 Udo Klimaschewski - * - * http://UdoJava.com/ - * http://about.me/udo.klimaschewski - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - */ -package com.udojava.evalex; - -import com.udojava.evalex.Expression.LazyNumber; - -/** - * Base interface which is required for all operators. - */ -public interface LazyOperator { - - - /** - * Gets the String that is used to denote the operator in the expression. - * - * @return The String that is used to denote the operator in the expression. - */ - String getOper(); - - - /** - * Gets the precedence value of this operator. - * - * @return the precedence value of this operator. - */ - int getPrecedence(); - - - /** - * Gets whether this operator is left associative (true) or if this operator is right - * associative (false). - * - * @return true if this operator is left associative. - */ - boolean isLeftAssoc(); - - - /** - * Gets whether this operator evaluates to a boolean expression. - * - * @return true if this operator evaluates to a boolean - * expression. - */ - boolean isBooleanOperator(); - - - /** - * Gets whether this operator is unary or not. - * - * @return true if this operator expects 1 operand, otherwise - * false if this operator expects 2 operands. - */ - boolean isUnaryOperator(); - - - /** - * Implementation for this operator. - * - * @param v1 Operand 1. - * @param v2 Operand 2. - * @return The result of the operation. - */ - LazyNumber eval(LazyNumber v1, LazyNumber v2); -} diff --git a/src/main/java/com/udojava/evalex/Operator.java b/src/main/java/com/udojava/evalex/Operator.java deleted file mode 100644 index 6036adf..0000000 --- a/src/main/java/com/udojava/evalex/Operator.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2018 Udo Klimaschewski - * - * http://UdoJava.com/ - * http://about.me/udo.klimaschewski - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - */ -package com.udojava.evalex; - -import java.math.BigDecimal; - -/** - * Base interface which is required for all operators. Abstract class AbstractOperator accepts - * postfix unary operators with the second operand v2=null. - */ -public interface Operator extends LazyOperator { - - - /** - * Implementation for this operator. - * - * @param v1 Operand 1. - * @param v2 Operand 2. Null for postfix unary operators. - * @return The result of the operation. - */ - BigDecimal eval(BigDecimal v1, BigDecimal v2); -} diff --git a/src/main/java/com/udojava/evalex/TokenizerException.java b/src/main/java/com/udojava/evalex/TokenizerException.java deleted file mode 100644 index 749f018..0000000 --- a/src/main/java/com/udojava/evalex/TokenizerException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.udojava.evalex; - -public class TokenizerException extends Expression.ExpressionException { - public TokenizerException(String message, int characterPosition) { - super(message, characterPosition); - } -} diff --git a/src/main/java9/module-info.java b/src/main/java9/module-info.java deleted file mode 100644 index a8fe122..0000000 --- a/src/main/java9/module-info.java +++ /dev/null @@ -1,3 +0,0 @@ -module com.udojava.evalex { - exports com.udojava.evalex; -} diff --git a/src/test/java/com/ezylang/evalex/BaseEvaluationTest.java b/src/test/java/com/ezylang/evalex/BaseEvaluationTest.java new file mode 100644 index 0000000..69a29c1 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/BaseEvaluationTest.java @@ -0,0 +1,50 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.config.TestConfigurationProvider; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.ParseException; + +public abstract class BaseEvaluationTest { + + protected void assertExpressionHasExpectedResult(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertThat( + evaluate( + expression, + TestConfigurationProvider.StandardConfigurationWithAdditionalTestOperators) + .getStringValue()) + .isEqualTo(expectedResult); + } + + protected void assertExpressionHasExpectedResult( + String expression, String expectedResult, ExpressionConfiguration expressionConfiguration) + throws EvaluationException, ParseException { + assertThat(evaluate(expression, expressionConfiguration).getStringValue()) + .isEqualTo(expectedResult); + } + + private EvaluationValue evaluate(String expressionString, ExpressionConfiguration configuration) + throws EvaluationException, ParseException { + Expression expression = new Expression(expressionString, configuration); + + return expression.evaluate(); + } +} diff --git a/src/test/java/com/ezylang/evalex/BaseExceptionTest.java b/src/test/java/com/ezylang/evalex/BaseExceptionTest.java new file mode 100644 index 0000000..179ad66 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/BaseExceptionTest.java @@ -0,0 +1,42 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class BaseExceptionTest { + + @Test + void testCreation() { + BaseException exception = new BaseException(2, 4, "test", "message"); + + assertThat(exception.getStartPosition()).isEqualTo(2); + assertThat(exception.getEndPosition()).isEqualTo(4); + assertThat(exception.getTokenString()).isEqualTo("test"); + assertThat(exception.getMessage()).isEqualTo("message"); + } + + @Test + void testToString() { + BaseException exception = new BaseException(2, 4, "test", "message"); + + assertThat(exception) + .hasToString( + "BaseException(startPosition=2, endPosition=4, tokenString=test, message=message)"); + } +} diff --git a/src/test/java/com/ezylang/evalex/BaseExpressionEvaluatorTest.java b/src/test/java/com/ezylang/evalex/BaseExpressionEvaluatorTest.java new file mode 100644 index 0000000..6553db8 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/BaseExpressionEvaluatorTest.java @@ -0,0 +1,35 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.config.TestConfigurationProvider; +import com.ezylang.evalex.parser.ParseException; + +public abstract class BaseExpressionEvaluatorTest { + + final ExpressionConfiguration configuration = + TestConfigurationProvider.StandardConfigurationWithAdditionalTestOperators; + + String evaluate(String expressionString) throws ParseException, EvaluationException { + Expression expression = createExpression(expressionString); + return expression.evaluate().getStringValue(); + } + + Expression createExpression(String expressionString) { + return new Expression(expressionString, configuration); + } +} diff --git a/src/test/java/com/ezylang/evalex/EvalEx2CompatibilityTest.java b/src/test/java/com/ezylang/evalex/EvalEx2CompatibilityTest.java new file mode 100644 index 0000000..9089020 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/EvalEx2CompatibilityTest.java @@ -0,0 +1,230 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.parser.ParseException; +import java.math.BigDecimal; +import java.math.MathContext; +import org.junit.jupiter.api.Test; + +class EvalEx2CompatibilityTest { + + @Test + void testContinuousUnary() throws EvaluationException, ParseException { + assertThat(evaluateToNumber("++1")).isEqualByComparingTo("1"); + assertThat(evaluateToNumber("--1")).isEqualByComparingTo("1"); + assertThat(evaluateToNumber("+-1")).isEqualByComparingTo("-1"); + assertThat(evaluateToNumber("-+1")).isEqualByComparingTo("-1"); + assertThat(evaluateToNumber("1-+1")).isEqualByComparingTo("0"); + assertThat(evaluateToNumber("-+---+++--++-1")).isEqualByComparingTo("-1"); + assertThat(evaluateToNumber("1--++++---2+-+----1")).isEqualByComparingTo("-2"); + } + + @Test + void testBrackets() throws EvaluationException, ParseException { + assertThat(evaluateToNumber("(1+2)")).isEqualByComparingTo("3"); + assertThat(evaluateToNumber("((1+2))")).isEqualByComparingTo("3"); + assertThat(evaluateToNumber("(((1+2)))")).isEqualByComparingTo("3"); + assertThat(evaluateToNumber("(1+2)*(1+2)")).isEqualByComparingTo("9"); + assertThat(evaluateToNumber("(1+2)*(1+2)+1")).isEqualByComparingTo("10"); + assertThat(evaluateToNumber("(1+2)*((1+2)+1)")).isEqualByComparingTo("12"); + } + + @Test + void testSimple() throws EvaluationException, ParseException { + assertThat(evaluateToNumber("1+2")).isEqualByComparingTo("3"); + assertThat(evaluateToNumber("4/2")).isEqualByComparingTo("2"); + assertThat(evaluateToNumber("3+4/2")).isEqualByComparingTo("5"); + assertThat(evaluateToNumber("(3+4)/2")).isEqualByComparingTo("3.5"); + assertThat(evaluateToNumber("4.2*1.9")).isEqualByComparingTo("7.98"); + assertThat(evaluateToNumber("8%3")).isEqualByComparingTo("2"); + assertThat(evaluateToNumber("8%2")).isEqualByComparingTo("0"); + assertThat(evaluateToNumber("2*.1")).isEqualByComparingTo("0.2"); + } + + @Test + void testUnaryMinus() throws EvaluationException, ParseException { + assertThat(evaluateToNumber("-3")).isEqualByComparingTo("-3"); + assertThat(evaluateToNumber("-SQRT(4)")).isEqualByComparingTo("-2"); + assertThat(evaluateToNumber("-(5*3+(+10-13))")).isEqualByComparingTo("-12"); + assertThat(evaluateToNumber("-2+3/4*-1")).isEqualByComparingTo("-2.75"); + assertThat(evaluateToNumber("-3^2")).isEqualByComparingTo("9"); + assertThat(evaluateToNumber("4^-0.5")).isEqualByComparingTo("0.5"); + assertThat(evaluateToNumber("-2+3/4")).isEqualByComparingTo("-1.25"); + assertThat(evaluateToNumber("-(3+-4*-1/-2)")).isEqualByComparingTo("-1"); + assertThat(evaluateToNumber("2+-.2")).isEqualByComparingTo("1.8"); + } + + @Test + void testUnaryPlus() throws EvaluationException, ParseException { + assertThat(evaluateToNumber("+3")).isEqualByComparingTo("3"); + assertThat(evaluateToNumber("+(3-1+2)")).isEqualByComparingTo("4"); + assertThat(evaluateToNumber("+(3-(+1)+2)")).isEqualByComparingTo("4"); + assertThat(evaluateToNumber("+3^2")).isEqualByComparingTo("9"); + } + + @Test + void testPow() throws EvaluationException, ParseException { + assertThat(evaluateToNumber("2^4")).isEqualByComparingTo("16"); + assertThat(evaluateToNumber("2^8")).isEqualByComparingTo("256"); + assertThat(evaluateToNumber("3^2")).isEqualByComparingTo("9"); + assertThat(evaluateToNumber("2.5^2")).isEqualByComparingTo("6.25"); + assertThat(evaluateToNumber("2.6^3.5")).isEqualByComparingTo("28.34045"); + } + + @Test + void testSqrt() throws EvaluationException, ParseException { + assertThat(evaluateToNumber("SQRT(16)")).isEqualByComparingTo("4"); + assertThat(evaluateToNumber("SQRT(2)")).isEqualByComparingTo("1.4142135"); + + Expression expression = + new Expression( + "SQRT(2)", ExpressionConfiguration.builder().mathContext(new MathContext(128)).build()); + assertThat(expression.evaluate().getNumberValue()) + .isEqualByComparingTo( + "1.41421356237309504880168872420969807856967187537694807317667973799073247846210703885038753432764157273501384623091229702492483605"); + + assertThat(evaluateToNumber("SQRT(5)")).isEqualByComparingTo("2.2360679"); + assertThat(evaluateToNumber("SQRT(9875)")).isEqualByComparingTo("99.3730345"); + assertThat(evaluateToNumber("SQRT(5.55)")).isEqualByComparingTo("2.3558437"); + assertThat(evaluateToNumber("SQRT(0)")).isEqualByComparingTo("0"); + } + + @Test + void testTrigonometry() throws EvaluationException, ParseException { // NOSONAR - >=25 assertions + assertThat(evaluateToNumber("SIN(30)")).isEqualByComparingTo("0.5"); + assertThat(evaluateToNumber("COS(30)")).isEqualByComparingTo("0.8660254"); + assertThat(evaluateToNumber("TAN(30)")).isEqualByComparingTo("0.5773503"); + assertThat(evaluateToNumber("SINH(30)")).isEqualByComparingTo("5343237000000"); + assertThat(evaluateToNumber("COSH(30)")).isEqualByComparingTo("5343237000000"); + assertThat(evaluateToNumber("TANH(30)")).isEqualByComparingTo("1"); + assertThat(evaluateToNumber("RAD(30)")).isEqualByComparingTo("0.5235988"); + assertThat(evaluateToNumber("DEG(30)")).isEqualByComparingTo("1718.873"); + assertThat(evaluateToNumber("ATAN(0.5773503)")).isEqualByComparingTo("30"); + assertThat(evaluateToNumber("ATAN2(0.5773503, 1)")).isEqualByComparingTo("30"); + assertThat(evaluateToNumber("ATAN2(2, 3)")).isEqualByComparingTo("33.69007"); + assertThat(evaluateToNumber("ATAN2(2, -3)")).isEqualByComparingTo("146.3099"); + assertThat(evaluateToNumber("ATAN2(-2, -3)")).isEqualByComparingTo("-146.3099"); + assertThat(evaluateToNumber("ATAN2(-2, 3)")).isEqualByComparingTo("-33.69007"); + assertThat(evaluateToNumber("SEC(30)")).isEqualByComparingTo("1.154701"); + assertThat(evaluateToNumber("SEC(45)")).isEqualByComparingTo("1.414214"); + assertThat(evaluateToNumber("SEC(60)")).isEqualByComparingTo("2"); + assertThat(evaluateToNumber("SEC(75)")).isEqualByComparingTo("3.863703"); + assertThat(evaluateToNumber("CSC(30)")).isEqualByComparingTo("2"); + assertThat(evaluateToNumber("CSC(45)")).isEqualByComparingTo("1.414214"); + assertThat(evaluateToNumber("CSC(60)")).isEqualByComparingTo("1.154701"); + assertThat(evaluateToNumber("CSC(75)")).isEqualByComparingTo("1.035276"); + assertThat(evaluateToNumber("SECH(30)")).isEqualByComparingTo("0.0000000000001871525"); + assertThat(evaluateToNumber("SECH(45)")).isEqualByComparingTo("0.00000000000000000005725037"); + assertThat(evaluateToNumber("SECH(60)")) + .isEqualByComparingTo("0.00000000000000000000000001751302"); + assertThat(evaluateToNumber("SECH(75)")) + .isEqualByComparingTo("0.000000000000000000000000000000005357274"); + assertThat(evaluateToNumber("CSCH(30)")).isEqualByComparingTo("0.0000000000001871525"); + assertThat(evaluateToNumber("CSCH(45)")).isEqualByComparingTo("0.00000000000000000005725037"); + assertThat(evaluateToNumber("CSCH(60)")) + .isEqualByComparingTo("0.00000000000000000000000001751302"); + assertThat(evaluateToNumber("CSCH(75)")) + .isEqualByComparingTo("0.000000000000000000000000000000005357274"); + assertThat(evaluateToNumber("COT(30)")).isEqualByComparingTo("1.732051"); + assertThat(evaluateToNumber("COT(45)")).isEqualByComparingTo("1"); + assertThat(evaluateToNumber("COT(60)")).isEqualByComparingTo("0.5773503"); + assertThat(evaluateToNumber("COT(75)")).isEqualByComparingTo("0.2679492"); + assertThat(evaluateToNumber("ACOT(30)")).isEqualByComparingTo("1.909152"); + assertThat(evaluateToNumber("ACOT(45)")).isEqualByComparingTo("1.27303"); + assertThat(evaluateToNumber("ACOT(60)")).isEqualByComparingTo("0.9548413"); + assertThat(evaluateToNumber("ACOT(75)")).isEqualByComparingTo("0.7638985"); + assertThat(evaluateToNumber("COTH(30)")).isEqualByComparingTo("1"); + assertThat(evaluateToNumber("COTH(1.2)")).isEqualByComparingTo("1.199538"); + assertThat(evaluateToNumber("COTH(2.4)")).isEqualByComparingTo("1.016596"); + assertThat(evaluateToNumber("ASINH(30)")).isEqualByComparingTo("4.094622"); + assertThat(evaluateToNumber("ASINH(45)")).isEqualByComparingTo("4.499933"); + assertThat(evaluateToNumber("ASINH(60)")).isEqualByComparingTo("4.787561"); + assertThat(evaluateToNumber("ASINH(75)")).isEqualByComparingTo("5.01068"); + assertThat(evaluateToNumber("ACOSH(1)")).isEqualByComparingTo("0"); + assertThat(evaluateToNumber("ACOSH(30)")).isEqualByComparingTo("4.094067"); + assertThat(evaluateToNumber("ACOSH(45)")).isEqualByComparingTo("4.499686"); + assertThat(evaluateToNumber("ACOSH(60)")).isEqualByComparingTo("4.787422"); + assertThat(evaluateToNumber("ACOSH(75)")).isEqualByComparingTo("5.010591"); + assertThat(evaluateToNumber("ATANH(0)")).isEqualByComparingTo("0"); + assertThat(evaluateToNumber("ATANH(0.5)")).isEqualByComparingTo("0.5493061"); + assertThat(evaluateToNumber("ATANH(-0.5)")).isEqualByComparingTo("-0.5493061"); + } + + @Test + void testMinMaxAbs() throws EvaluationException, ParseException { + assertThat(evaluateToNumber("MAX(3.78787,3.78786)")).isEqualByComparingTo("3.78787"); + assertThat(evaluateToNumber("max(3.78786,3.78787)")).isEqualByComparingTo("3.78787"); + assertThat(evaluateToNumber("MIN(3.78787,3.78786)")).isEqualByComparingTo("3.78786"); + assertThat(evaluateToNumber("Min(3.78786,3.78787)")).isEqualByComparingTo("3.78786"); + assertThat(evaluateToNumber("aBs(-2.123)")).isEqualByComparingTo("2.123"); + assertThat(evaluateToNumber("abs(2.123)")).isEqualByComparingTo("2.123"); + } + + @Test + void testRounding() throws EvaluationException, ParseException { + assertThat(evaluateToNumber("round(3.78787,1)")).isEqualByComparingTo("3.8"); + assertThat(evaluateToNumber("round(3.78787,3)")).isEqualByComparingTo("3.788"); + assertThat(evaluateToNumber("round(3.7345,3)")).isEqualByComparingTo("3.734"); + assertThat(evaluateToNumber("round(-3.7345,3)")).isEqualByComparingTo("-3.734"); + assertThat(evaluateToNumber("round(-3.78787,2)")).isEqualByComparingTo("-3.79"); + assertThat(evaluateToNumber("round(123.78787,2)")).isEqualByComparingTo("123.79"); + assertThat(evaluateToNumber("floor(3.78787)")).isEqualByComparingTo("3"); + assertThat(evaluateToNumber("ceiling(3.78787)")).isEqualByComparingTo("4"); + assertThat(evaluateToNumber("floor(-2.1)")).isEqualByComparingTo("-3"); + assertThat(evaluateToNumber("ceiling(-2.1)")).isEqualByComparingTo("-2"); + } + + @Test + void testSciNotation() throws EvaluationException, ParseException { + // simple + assertThat(evaluateToNumber("1e10")).isEqualByComparingTo("10000000000"); + assertThat(evaluateToNumber("1E10")).isEqualByComparingTo("10000000000"); + assertThat(evaluateToNumber("123.456E3")).isEqualByComparingTo("123456"); + assertThat(evaluateToNumber("2.5e0")).isEqualByComparingTo("2.5"); + + // negative + assertThat(evaluateToNumber("1e-10")).isEqualByComparingTo("0.0000000001"); + assertThat(evaluateToNumber("1E-10")).isEqualByComparingTo("0.0000000001"); + assertThat(evaluateToNumber("2135E-4")).isEqualByComparingTo("0.2135"); + + // positive + assertThat(evaluateToNumber("1e+10")).isEqualByComparingTo("10000000000"); + assertThat(evaluateToNumber("1E+10")).isEqualByComparingTo("10000000000"); + + // combined + assertThat(evaluateToNumber("sqrt(152.399025e6)")).isEqualByComparingTo("12344.9989874"); + assertThat(evaluateToNumber("sin(3.e1)")).isEqualByComparingTo("0.5"); + assertThat(evaluateToNumber("sin( 3.e1)")).isEqualByComparingTo("0.5"); + assertThat(evaluateToNumber("sin(3.e1 )")).isEqualByComparingTo("0.5"); + assertThat(evaluateToNumber("sin( 3.e1 )")).isEqualByComparingTo("0.5"); + assertThat(evaluateToNumber("2.2e-16 * 10.2")).isEqualByComparingTo("2.244E-15"); + } + + private BigDecimal evaluateToNumber(String expression) + throws EvaluationException, ParseException { + + // Given an expression with EvalEx2 compatible math context + return new Expression( + expression, + ExpressionConfiguration.builder().mathContext(MathContext.DECIMAL32).build()) + .evaluate() + .getNumberValue(); + } +} diff --git a/src/test/java/com/ezylang/evalex/ExpressionEvaluationExceptionsTest.java b/src/test/java/com/ezylang/evalex/ExpressionEvaluationExceptionsTest.java new file mode 100644 index 0000000..23ded97 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/ExpressionEvaluationExceptionsTest.java @@ -0,0 +1,40 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.parser.ASTNode; +import com.ezylang.evalex.parser.Token; +import com.ezylang.evalex.parser.Token.TokenType; +import org.junit.jupiter.api.Test; + +class ExpressionEvaluationExceptionsTest { + + @Test + void testUnexpectedToken() { + Expression expression = new Expression("1"); + + assertThatThrownBy( + () -> { + ASTNode node = new ASTNode(new Token(1, "(", TokenType.BRACE_OPEN)); + expression.evaluateSubtree(node); + }) + .isInstanceOf(EvaluationException.class) + .hasMessage( + "Unexpected evaluation token: Token(startPosition=1, value=(, type=BRACE_OPEN)"); + } +} diff --git a/src/test/java/com/ezylang/evalex/ExpressionEvaluatorArrayTest.java b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorArrayTest.java new file mode 100644 index 0000000..f175197 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorArrayTest.java @@ -0,0 +1,130 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.parser.ParseException; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ExpressionEvaluatorArrayTest extends BaseExpressionEvaluatorTest { + + @Test + void testSimpleArray() throws ParseException, EvaluationException { + List array = List.of(new BigDecimal(99)); + Expression expression = createExpression("a[0]").with("a", array); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("99"); + } + + @Test + void testMultipleEntriesArray() throws ParseException, EvaluationException { + List array = Arrays.asList(new BigDecimal(2), new BigDecimal(4), new BigDecimal(6)); + Expression expression = createExpression("a[0]+a[1]+a[2]").with("a", array); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("12"); + } + + @Test + void testExpressionArray() throws ParseException, EvaluationException { + List array = List.of(new BigDecimal(3)); + Expression expression = createExpression("a[4-x]").with("a", array).and("x", new BigDecimal(4)); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("3"); + } + + @Test + void testNestedArray() throws ParseException, EvaluationException { + List arrayA = List.of(new BigDecimal(3)); + List arrayB = + Arrays.asList(new BigDecimal(2), new BigDecimal(4), new BigDecimal(6)); + Expression expression = + createExpression("a[b[6-4]-x]") + .with("a", arrayA) + .and("b", arrayB) + .and("x", new BigDecimal(6)); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("3"); + } + + @Test + void testStringArray() throws ParseException, EvaluationException { + List array = Arrays.asList("Hello", "beautiful", "world"); + Expression expression = createExpression("a[0] + \" \" + a[1] + \" \" + a[2]").with("a", array); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("Hello beautiful world"); + } + + @Test + void testBooleanArray() throws ParseException, EvaluationException { + List array = Arrays.asList(true, true, false); + Expression expression = createExpression("a[0] + \" \" + a[1] + \" \" + a[2]").with("a", array); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("true true false"); + } + + @Test + void testArrayOfArray() throws EvaluationException, ParseException { + List subArray1 = Arrays.asList(new BigDecimal(1), new BigDecimal(2)); + List subArray2 = Arrays.asList(new BigDecimal(4), new BigDecimal(8)); + + List> array = Arrays.asList(subArray1, subArray2); + + Expression expression1 = createExpression("a[0][0]").with("a", array); + Expression expression2 = createExpression("a[0][1]").with("a", array); + Expression expression3 = createExpression("a[1][0]").with("a", array); + Expression expression4 = createExpression("a[1][1]").with("a", array); + + assertThat(expression1.evaluate().getStringValue()).isEqualTo("1"); + assertThat(expression2.evaluate().getStringValue()).isEqualTo("2"); + assertThat(expression3.evaluate().getStringValue()).isEqualTo("4"); + assertThat(expression4.evaluate().getStringValue()).isEqualTo("8"); + } + + @Test + void testMixedArray() throws ParseException, EvaluationException { + List array = Arrays.asList("Hello", new BigDecimal(4), true); + Expression expression1 = createExpression("a[0]").with("a", array); + Expression expression2 = createExpression("a[1]").with("a", array); + Expression expression3 = createExpression("a[2]").with("a", array); + + assertThat(expression1.evaluate().getStringValue()).isEqualTo("Hello"); + assertThat(expression2.evaluate().getStringValue()).isEqualTo("4"); + assertThat(expression3.evaluate().getStringValue()).isEqualTo("true"); + } + + @Test + void testThrowsUnsupportedDataTypeForArray() { + assertThatThrownBy(() -> createExpression("a[0]").with("a", "aString").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Unsupported data types in operation"); + } + + @Test + void testThrowsUnsupportedDataTypeForIndex() { + assertThatThrownBy( + () -> { + List array = List.of("Hello"); + createExpression("a[b]").with("a", array).and("b", "anotherString").evaluate(); + }) + .isInstanceOf(EvaluationException.class) + .hasMessage("Unsupported data types in operation"); + } +} diff --git a/src/test/java/com/ezylang/evalex/ExpressionEvaluatorCombinedTest.java b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorCombinedTest.java new file mode 100644 index 0000000..9f19456 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorCombinedTest.java @@ -0,0 +1,47 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.parser.ParseException; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ExpressionEvaluatorCombinedTest extends BaseExpressionEvaluatorTest { + + @Test + void testOrderPositionExample() throws ParseException, EvaluationException { + Map order = new HashMap<>(); + order.put("id", 12345); + order.put("name", "Mary"); + Map position = new HashMap<>(); + position.put("article", 3114); + position.put("amount", 3); + position.put("price", new BigDecimal("14.95")); + order.put("positions", List.of(position)); + + Expression expression = + new Expression("order.positions[x].amount * order.positions[x].price") + .with("order", order) + .and("x", 0); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("44.85"); + } +} diff --git a/src/test/java/com/ezylang/evalex/ExpressionEvaluatorConstantsTest.java b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorConstantsTest.java new file mode 100644 index 0000000..34432bd --- /dev/null +++ b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorConstantsTest.java @@ -0,0 +1,65 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.ParseException; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ExpressionEvaluatorConstantsTest extends BaseExpressionEvaluatorTest { + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "TRUE : true", + "true : true", + "False : false", + "PI : " + + " 3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679", + "e : 2.71828182845904523536028747135266249775724709369995957496696762772407663", + }) + void testDefaultConstants(String expression, String expectedResult) + throws ParseException, EvaluationException { + assertThat(evaluate(expression)).isEqualTo(expectedResult); + } + + @Test + void testCustomConstants() throws EvaluationException, ParseException { + Map constants = + new HashMap<>() { + { + put("A", new EvaluationValue(new BigDecimal("2.5"))); + put("B", new EvaluationValue(new BigDecimal("3.9"))); + } + }; + + ExpressionConfiguration configuration = + ExpressionConfiguration.builder().defaultConstants(constants).build(); + + Expression expression = new Expression("a+b", configuration); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("6.4"); + } +} diff --git a/src/test/java/com/ezylang/evalex/ExpressionEvaluatorDecimalPlacesTest.java b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorDecimalPlacesTest.java new file mode 100644 index 0000000..49628a9 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorDecimalPlacesTest.java @@ -0,0 +1,166 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.parser.ParseException; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ExpressionEvaluatorDecimalPlacesTest extends BaseExpressionEvaluatorTest { + + @Test + void testDefaultNoRoundingLiteral() throws ParseException, EvaluationException { + assertThat(evaluate("2.12345")).isEqualTo("2.12345"); + } + + @Test + void testDefaultNoRoundingVariable() throws ParseException, EvaluationException { + Expression expression = new Expression("a").with("a", new BigDecimal("2.12345")); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("2.12345"); + } + + @Test + void testDefaultNoRoundingInfixOperator() throws ParseException, EvaluationException { + assertThat(evaluate("2.12345+1.54321")).isEqualTo("3.66666"); + } + + @Test + void testDefaultNoRoundingPrefixOperator() throws ParseException, EvaluationException { + assertThat(evaluate("-2.12345")).isEqualTo("-2.12345"); + } + + @Test + void testDefaultNoRoundingFunction() throws ParseException, EvaluationException { + assertThat(evaluate("SUM(2.12345,1.54321)")).isEqualTo("3.66666"); + } + + @Test + void testDefaultNoRoundingArray() throws ParseException, EvaluationException { + List array = List.of(new BigDecimal("1.12345")); + Expression expression = createExpression("a[0]").with("a", array); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("1.12345"); + } + + @Test + void testDefaultNoRoundingStructure() throws ParseException, EvaluationException { + Map structure = + new HashMap<>() { + { + put("b", new BigDecimal("1.12345")); + } + }; + + Expression expression = createExpression("a.b").with("a", structure); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("1.12345"); + } + + @Test + void testCustomRoundingDecimalsLiteral() throws ParseException, EvaluationException { + ExpressionConfiguration config = + ExpressionConfiguration.builder().decimalPlacesRounding(2).build(); + Expression expression = new Expression("2.12345", config); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("2.12"); + } + + @Test + void testCustomRoundingDecimalsVariable() throws ParseException, EvaluationException { + ExpressionConfiguration config = + ExpressionConfiguration.builder().decimalPlacesRounding(2).build(); + Expression expression = new Expression("a", config).with("a", new BigDecimal("2.126")); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("2.13"); + } + + @Test + void testCustomRoundingDecimalsInfixOperator() throws ParseException, EvaluationException { + ExpressionConfiguration config = + ExpressionConfiguration.builder().decimalPlacesRounding(3).build(); + Expression expression = new Expression("2.12345+1.54321", config); + + // literals are rounded first, the added and rounded again. + assertThat(expression.evaluate().getStringValue()).isEqualTo("3.666"); + } + + @Test + void testCustomRoundingDecimalsPrefixOperator() throws ParseException, EvaluationException { + ExpressionConfiguration config = + ExpressionConfiguration.builder().decimalPlacesRounding(3).build(); + Expression expression = new Expression("-2.12345", config); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("-2.123"); + } + + @Test + void testCustomRoundingDecimalsFunction() throws ParseException, EvaluationException { + ExpressionConfiguration config = + ExpressionConfiguration.builder().decimalPlacesRounding(3).build(); + Expression expression = new Expression("SUM(2.12345,1.54321)", config); + + // literals are rounded first, the added and rounded again. + assertThat(expression.evaluate().getStringValue()).isEqualTo("3.666"); + } + + @Test + void testCustomRoundingDecimalsArray() throws ParseException, EvaluationException { + ExpressionConfiguration config = + ExpressionConfiguration.builder().decimalPlacesRounding(3).build(); + List array = List.of(new BigDecimal("1.12345")); + Expression expression = new Expression("a[0]", config).with("a", array); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("1.123"); + } + + @Test + void testCustomRoundingStructure() throws ParseException, EvaluationException { + ExpressionConfiguration config = + ExpressionConfiguration.builder().decimalPlacesRounding(3).build(); + Map structure = + new HashMap<>() { + { + put("b", new BigDecimal("1.12345")); + } + }; + + Expression expression = new Expression("a.b", config).with("a", structure); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("1.123"); + } + + @Test + void testDefaultStripZeros() throws EvaluationException, ParseException { + Expression expression = new Expression("9.000"); + assertThat(expression.evaluate().getNumberValue()).isEqualTo("9"); + } + + @Test + void testDoNotStripZeros() throws EvaluationException, ParseException { + ExpressionConfiguration config = + ExpressionConfiguration.builder().stripTrailingZeros(false).build(); + + Expression expression = new Expression("9.000", config); + assertThat(expression.evaluate().getNumberValue()).isEqualTo("9.000"); + } +} diff --git a/src/test/java/com/ezylang/evalex/ExpressionEvaluatorFunctionTest.java b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorFunctionTest.java new file mode 100644 index 0000000..7f8657c --- /dev/null +++ b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorFunctionTest.java @@ -0,0 +1,50 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.parser.ParseException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ExpressionEvaluatorFunctionTest extends BaseExpressionEvaluatorTest { + + @Test + void testSingleParameterFunction() throws ParseException, EvaluationException { + assertThat(evaluate("FACT(3)")).isEqualTo("6"); + } + + @Test + void testDualParameterFunction() throws ParseException, EvaluationException { + assertThat(evaluate("ROUND(3.123, 2)")).isEqualTo("3.12"); + } + + @Test + void testNestedFunctions() throws ParseException, EvaluationException { + assertThat(evaluate("FACT(ROUND(3.95, 0))")).isEqualTo("24"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = {"MAX(5) : 5", "MAX(4,6,3,8) : 8"}) + void testVarArgFunction(String expressionString, String expectedResult) + throws ParseException, EvaluationException { + assertThat(evaluate(expressionString)).isEqualTo(expectedResult); + } +} diff --git a/src/test/java/com/ezylang/evalex/ExpressionEvaluatorHexadecimalTest.java b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorHexadecimalTest.java new file mode 100644 index 0000000..a6cd4ce --- /dev/null +++ b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorHexadecimalTest.java @@ -0,0 +1,41 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.parser.ParseException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ExpressionEvaluatorHexadecimalTest extends BaseExpressionEvaluatorTest { + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "0x0 : 0 ", + "0x1 : 1", + "0xa : 10", + "0x10 + 0xaa : 186", + "0xff + 0xff : 510", + "0X87FAB987 + 0x123 : 2281355946" + }) + void testHexadecimalLiteralsEvaluation(String expression, String expectedResult) + throws ParseException, EvaluationException { + assertThat(evaluate(expression)).isEqualTo(expectedResult); + } +} diff --git a/src/test/java/com/ezylang/evalex/ExpressionEvaluatorPowerOfTest.java b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorPowerOfTest.java new file mode 100644 index 0000000..e74366a --- /dev/null +++ b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorPowerOfTest.java @@ -0,0 +1,43 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.operators.OperatorIfc; +import com.ezylang.evalex.parser.ParseException; +import org.junit.jupiter.api.Test; + +class ExpressionEvaluatorPowerOfTest extends BaseExpressionEvaluatorTest { + + @Test + void testPrecedenceDefault() throws ParseException, EvaluationException { + assertThat(evaluate("-2^2")).isEqualTo("4"); + } + + @Test + void testPrecedenceHigher() throws ParseException, EvaluationException { + ExpressionConfiguration config = + ExpressionConfiguration.builder() + .powerOfPrecedence(OperatorIfc.OPERATOR_PRECEDENCE_POWER_HIGHER) + .build(); + + Expression expression = new Expression("-2^2", config); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("-4"); + } +} diff --git a/src/test/java/com/ezylang/evalex/ExpressionEvaluatorScientificTest.java b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorScientificTest.java new file mode 100644 index 0000000..b6e7044 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorScientificTest.java @@ -0,0 +1,46 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.parser.ParseException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ExpressionEvaluatorScientificTest extends BaseExpressionEvaluatorTest { + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "2e3 : 2000 ", + "2e10 : 20000000000 ", + "2E3 : 2000", + "0.5e2 : 50", + "0.35E4 + 0.5e1 : 3505", + "1e-10 : 0.0000000001", + "1E-10 : 0.0000000001", + "2135E-4 : 0.2135", + "1e+10 : 10000000000", + "1E+10 : 10000000000", + "2135E+4 : 21350000" + }) + void testScientificLiteralsEvaluation(String expression, String expectedResult) + throws ParseException, EvaluationException { + assertThat(evaluate(expression)).isEqualTo(expectedResult); + } +} diff --git a/src/test/java/com/ezylang/evalex/ExpressionEvaluatorSimpleTest.java b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorSimpleTest.java new file mode 100644 index 0000000..a076638 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorSimpleTest.java @@ -0,0 +1,100 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.parser.ParseException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ExpressionEvaluatorSimpleTest extends BaseExpressionEvaluatorTest { + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "1 : 1", + "1+1 : 2", + "1-1 : 0", + "1-2 : -1", + "5-3-1 : 1", + "-1+1 : 0", + "1+ -1 : 0", + "-1 + -1 : -2", + "1+2+3 : 6", + "8-4+1-2+4-2 : 5" + }) + void testSimpleAdditiveEvaluation(String expression, String expectedResult) + throws ParseException, EvaluationException { + assertThat(evaluate(expression)).isEqualTo(expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "2/2 : 1", + "4/2 : 2", + "2*2 : 4", + "2*4 : 8", + "6/3*2 : 4", + "6*2/3 : 4", + "-1 * -1 : 1", + "-2 * 2 : -4", + "4*2/4 : 2", + "4*2*3/2/2 : 6" + }) + void testSimpleMultiplicativeEvaluation(String expression, String expectedResult) + throws ParseException, EvaluationException { + assertThat(evaluate(expression)).isEqualTo(expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "2+3*2 : 8", + "2+3*2+2 : 10", + "2+6/3+1 : 5", + "2+6/3-1 : 3", + "1+2+3+4/2+4-2 : 10", + "2*2*2+4/2 : 10", + "1*2-2*2/2+8 : 8" + }) + void testSimpleMixedPrecedence(String expression, String expectedResult) + throws ParseException, EvaluationException { + assertThat(evaluate(expression)).isEqualTo(expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "(3) : 3", + "(2+3)*2 : 10", + "(2+3)*(2+2) : 20", + "(2+6)/2+1 : 5", + "(2+6)/(3-1) : 4", + "(((1+2)+3)+4)/(2+4-4) : 5", + "2*2*((2+4)/2) : 12", + "1*(2-2*2)/2+8 : 7" + }) + void testSimpleBraces(String expression, String expectedResult) + throws ParseException, EvaluationException { + assertThat(evaluate(expression)).isEqualTo(expectedResult); + } +} diff --git a/src/test/java/com/ezylang/evalex/ExpressionEvaluatorSimpleVariablesTest.java b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorSimpleVariablesTest.java new file mode 100644 index 0000000..b665f8c --- /dev/null +++ b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorSimpleVariablesTest.java @@ -0,0 +1,77 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.ParseException; +import java.math.BigDecimal; +import org.junit.jupiter.api.Test; + +class ExpressionEvaluatorSimpleVariablesTest extends BaseExpressionEvaluatorTest { + + @Test + void testSingleStringVariable() throws ParseException, EvaluationException { + Expression expression = createExpression("a").with("a", "hello"); + EvaluationValue result = expression.evaluate(); + assertThat(result.isStringValue()).isTrue(); + assertThat(result.getStringValue()).isEqualTo("hello"); + } + + @Test + void testSingleNumberVariable() throws ParseException, EvaluationException { + Expression expression = createExpression("a").with("a", BigDecimal.valueOf(9)); + EvaluationValue result = expression.evaluate(); + assertThat(result.isNumberValue()).isTrue(); + assertThat(result.getNumberValue()).isEqualTo(BigDecimal.valueOf(9)); + } + + @Test + void testNumbers() throws ParseException, EvaluationException { + Expression expression = + createExpression("(a+b)*(a-b)") + .with("a", BigDecimal.valueOf(9)) + .and("b", BigDecimal.valueOf(5)); + EvaluationValue result = expression.evaluate(); + assertThat(result.isNumberValue()).isTrue(); + assertThat(result.getNumberValue()).isEqualTo(BigDecimal.valueOf(56)); + } + + @Test + void testStrings() throws ParseException, EvaluationException { + Expression expression = + createExpression("prefix+infix+postfix") + .with("prefix", "Hello") + .and("infix", " ") + .and("postfix", "world"); + EvaluationValue result = expression.evaluate(); + assertThat(result.isStringValue()).isTrue(); + assertThat(result.getStringValue()).isEqualTo("Hello world"); + } + + @Test + void testStringNumberCombined() throws ParseException, EvaluationException { + Expression expression = + createExpression("prefix+infix+postfix") + .with("prefix", "Hello") + .and("infix", BigDecimal.valueOf(2)) + .and("postfix", "world"); + EvaluationValue result = expression.evaluate(); + assertThat(result.isStringValue()).isTrue(); + assertThat(result.getStringValue()).isEqualTo("Hello2world"); + } +} diff --git a/src/test/java/com/ezylang/evalex/ExpressionEvaluatorStringTest.java b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorStringTest.java new file mode 100644 index 0000000..d60b590 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorStringTest.java @@ -0,0 +1,64 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.parser.ParseException; +import org.junit.jupiter.api.Test; + +class ExpressionEvaluatorStringTest extends BaseExpressionEvaluatorTest { + + @Test + void testStringConcatenation() throws ParseException, EvaluationException { + assertThat(evaluate("\"Hello\"+\" world\"")).isEqualTo("Hello world"); + } + + @Test + void testStringAndNumberConcatenation() throws ParseException, EvaluationException { + assertThat(evaluate("\"Test\"+1")).isEqualTo("Test1"); + } + + @Test + void testStringAndDoubleNumberConcatenation() throws ParseException, EvaluationException { + assertThat(evaluate("\"Test\"+1+2")).isEqualTo("Test12"); + } + + @Test + void testNumberAndStringConcatenation() throws ParseException, EvaluationException { + assertThat(evaluate("1+\"Test\"")).isEqualTo("1Test"); + } + + @Test + void testDoubleNumberAndStringConcatenation() throws ParseException, EvaluationException { + assertThat(evaluate("1+2+\"Test\"")).isEqualTo("3Test"); + } + + @Test + void testInnerNumberConcatenation() throws ParseException, EvaluationException { + assertThat(evaluate("\"Start\"+1+1+\"End\"")).isEqualTo("Start11End"); + } + + @Test + void testInnerTripleNumberConcatenation() throws ParseException, EvaluationException { + assertThat(evaluate("\"Start\"+1+2+1+\"End\"")).isEqualTo("Start121End"); + } + + @Test + void testInnerNumberConcatenationWithBraces() throws ParseException, EvaluationException { + assertThat(evaluate("\"Start\"+(1+1)+\"End\"")).isEqualTo("Start2End"); + } +} diff --git a/src/test/java/com/ezylang/evalex/ExpressionEvaluatorStructureTest.java b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorStructureTest.java new file mode 100644 index 0000000..663dcef --- /dev/null +++ b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorStructureTest.java @@ -0,0 +1,67 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.parser.ParseException; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ExpressionEvaluatorStructureTest extends BaseExpressionEvaluatorTest { + + @Test + void testSimpleStructure() throws ParseException, EvaluationException { + Map structure = + new HashMap<>() { + { + put("b", new BigDecimal(99)); + } + }; + + Expression expression = createExpression("a.b").with("a", structure); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("99"); + } + + @Test + void testTripleStructure() throws ParseException, EvaluationException { + Map> structure = new HashMap<>(); + + Map subStructure = + new HashMap<>() { + { + put("c", new BigDecimal(95)); + } + }; + + structure.put("b", subStructure); + + Expression expression = createExpression("a.b.c").with("a", structure); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("95"); + } + + @Test + void testThrowsUnsupportedDataTypeForStructure() { + assertThatThrownBy(() -> createExpression("a.b").with("a", "aString").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Unsupported data types in operation"); + } +} diff --git a/src/test/java/com/ezylang/evalex/ExpressionTest.java b/src/test/java/com/ezylang/evalex/ExpressionTest.java new file mode 100644 index 0000000..e0ffad5 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/ExpressionTest.java @@ -0,0 +1,148 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.ASTNode; +import com.ezylang.evalex.parser.ParseException; +import java.math.MathContext; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ExpressionTest { + + @Test + void testExpressionDefaults() { + Expression expression = new Expression("a+b"); + + assertThat(expression.getExpressionString()).isEqualTo("a+b"); + assertThat(expression.getConfiguration().getMathContext()) + .isEqualTo(ExpressionConfiguration.DEFAULT_MATH_CONTEXT); + assertThat(expression.getConfiguration().getFunctionDictionary().hasFunction("SUM")).isTrue(); + assertThat(expression.getConfiguration().getOperatorDictionary().hasInfixOperator("+")) + .isTrue(); + assertThat(expression.getConfiguration().getOperatorDictionary().hasPrefixOperator("+")) + .isTrue(); + assertThat(expression.getConfiguration().getOperatorDictionary().hasPostfixOperator("+")) + .isFalse(); + } + + @Test + void testValidateOK() throws ParseException { + new Expression("1+1").validate(); + } + + @Test + void testValidateFail() { + assertThatThrownBy(() -> new Expression("2#3").validate()) + .isInstanceOf(ParseException.class) + .hasMessage("Undefined operator '#'"); + } + + @Test + void testExpressionNode() throws ParseException, EvaluationException { + Expression expression = new Expression("a*b"); + ASTNode subExpression = expression.createExpressionNode("4+3"); + + EvaluationValue result = expression.with("a", 2).and("b", subExpression).evaluate(); + + assertThat(result.getStringValue()).isEqualTo("14"); + } + + @Test + void testWithValues() throws ParseException, EvaluationException { + Expression expression = new Expression("(a + b) * (a - b)"); + + Map values = new HashMap<>(); + values.put("a", 3.5); + values.put("b", 2.5); + + EvaluationValue result = expression.withValues(values).evaluate(); + + assertThat(result.getStringValue()).isEqualTo("6"); + } + + @Test + void testDefaultExpressionOwnsOwnConfigurationEntries() { + Expression expression1 = new Expression("1+1"); + Expression expression2 = new Expression("1+1"); + + assertThat(expression1.getDataAccessor()).isNotSameAs(expression2.getDataAccessor()); + assertThat(expression1.getConfiguration().getOperatorDictionary()) + .isNotSameAs(expression2.getConfiguration().getOperatorDictionary()); + assertThat(expression1.getConfiguration().getFunctionDictionary()) + .isNotSameAs(expression2.getConfiguration().getFunctionDictionary()); + assertThat(expression1.getConfiguration().getDefaultConstants()) + .isNotSameAs(expression2.getConfiguration().getDefaultConstants()); + } + + @Test + void testDoubleConverterDefaultMathContext() { + Expression defaultMathContextExpression = new Expression("1"); + assertThat(defaultMathContextExpression.convertDoubleValue(1.67987654321).getNumberValue()) + .isEqualByComparingTo("1.67987654321"); + } + + @Test + void testDoubleConverterLimitedMathContext() { + Expression limitedMathContextExpression = + new Expression( + "1", ExpressionConfiguration.builder().mathContext(new MathContext(3)).build()); + assertThat(limitedMathContextExpression.convertDoubleValue(1.6789).getNumberValue()) + .isEqualByComparingTo("1.68"); + } + + @Test + void testGetAllASTNodes() throws ParseException { + Expression expression = new Expression("1+2/3"); + List nodes = expression.getAllASTNodes(); + assertThat(nodes.get(0).getToken().getValue()).isEqualTo("+"); + assertThat(nodes.get(1).getToken().getValue()).isEqualTo("1"); + assertThat(nodes.get(2).getToken().getValue()).isEqualTo("/"); + assertThat(nodes.get(3).getToken().getValue()).isEqualTo("2"); + assertThat(nodes.get(4).getToken().getValue()).isEqualTo("3"); + } + + @Test + void testGetUsedVariables() throws ParseException { + Expression expression = new Expression("a/2*PI+MIN(e,b)"); + assertThat(expression.getUsedVariables()).containsExactlyInAnyOrder("a", "b"); + } + + @Test + void testGetUsedVariablesLongNames() throws ParseException { + Expression expression = new Expression("var1/2*PI+MIN(var2,var3)"); + assertThat(expression.getUsedVariables()).containsExactlyInAnyOrder("var1", "var2", "var3"); + } + + @Test + void testGetUsedVariablesNoVariables() throws ParseException { + Expression expression = new Expression("1/2"); + assertThat(expression.getUsedVariables()).isEmpty(); + } + + @Test + void testGetUsedVariablesCaseSensitivity() throws ParseException { + Expression expression = new Expression("a+B*b-A/PI*(1/2)*pi+e-E+a"); + assertThat(expression.getUsedVariables()).containsExactlyInAnyOrder("a", "b"); + } +} diff --git a/src/test/java/com/ezylang/evalex/config/ExpressionConfigurationTest.java b/src/test/java/com/ezylang/evalex/config/ExpressionConfigurationTest.java new file mode 100644 index 0000000..31fce8f --- /dev/null +++ b/src/test/java/com/ezylang/evalex/config/ExpressionConfigurationTest.java @@ -0,0 +1,178 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.config.TestConfigurationProvider.DummyFunction; +import com.ezylang.evalex.data.DataAccessorIfc; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.data.MapBasedDataAccessor; +import com.ezylang.evalex.operators.OperatorIfc; +import com.ezylang.evalex.operators.arithmetic.InfixPlusOperator; +import java.math.MathContext; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class ExpressionConfigurationTest { + + @Test + void testDefaultSetup() { + ExpressionConfiguration configuration = ExpressionConfiguration.defaultConfiguration(); + + assertThat(configuration.getMathContext()) + .isEqualTo(ExpressionConfiguration.DEFAULT_MATH_CONTEXT); + assertThat(configuration.getOperatorDictionary()) + .isInstanceOf(MapBasedOperatorDictionary.class); + assertThat(configuration.getFunctionDictionary()) + .isInstanceOf(MapBasedFunctionDictionary.class); + assertThat(configuration.getDataAccessorSupplier().get()) + .isInstanceOf(MapBasedDataAccessor.class); + assertThat(configuration.isArraysAllowed()).isTrue(); + assertThat(configuration.isStructuresAllowed()).isTrue(); + assertThat(configuration.isImplicitMultiplicationAllowed()).isTrue(); + assertThat(configuration.getPowerOfPrecedence()) + .isEqualTo(OperatorIfc.OPERATOR_PRECEDENCE_POWER); + assertThat(configuration.getDefaultConstants()) + .containsAllEntriesOf(ExpressionConfiguration.StandardConstants); + assertThat(configuration.getDecimalPlacesRounding()) + .isEqualTo(ExpressionConfiguration.DECIMAL_PLACES_ROUNDING_UNLIMITED); + assertThat(configuration.isStripTrailingZeros()).isTrue(); + } + + @Test + void testWithAdditionalOperators() { + ExpressionConfiguration configuration = + ExpressionConfiguration.defaultConfiguration() + .withAdditionalOperators( + Map.entry("ADDED1", new InfixPlusOperator()), + Map.entry("ADDED2", new InfixPlusOperator())); + + assertThat(configuration.getOperatorDictionary().hasInfixOperator("ADDED1")).isTrue(); + assertThat(configuration.getOperatorDictionary().hasInfixOperator("ADDED2")).isTrue(); + } + + @Test + void testWithAdditionalFunctions() { + ExpressionConfiguration configuration = + ExpressionConfiguration.defaultConfiguration() + .withAdditionalFunctions( + Map.entry("ADDED1", new DummyFunction()), Map.entry("ADDED2", new DummyFunction())); + + assertThat(configuration.getFunctionDictionary().hasFunction("ADDED1")).isTrue(); + assertThat(configuration.getFunctionDictionary().hasFunction("ADDED2")).isTrue(); + } + + @Test + void testCustomMathContext() { + ExpressionConfiguration configuration = + ExpressionConfiguration.builder().mathContext(MathContext.DECIMAL32).build(); + + assertThat(configuration.getMathContext()).isEqualTo(MathContext.DECIMAL32); + } + + @Test + void testCustomOperatorDictionary() { + OperatorDictionaryIfc mockedOperatorDictionary = Mockito.mock(OperatorDictionaryIfc.class); + + ExpressionConfiguration configuration = + ExpressionConfiguration.builder().operatorDictionary(mockedOperatorDictionary).build(); + + assertThat(configuration.getOperatorDictionary()).isEqualTo(mockedOperatorDictionary); + } + + @Test + void testCustomFunctionDictionary() { + FunctionDictionaryIfc mockedFunctionDictionary = Mockito.mock(FunctionDictionaryIfc.class); + + ExpressionConfiguration configuration = + ExpressionConfiguration.builder().functionDictionary(mockedFunctionDictionary).build(); + + assertThat(configuration.getFunctionDictionary()).isEqualTo(mockedFunctionDictionary); + } + + @Test + void testCustomDataAccessorSupplier() { + DataAccessorIfc mockedDataAccessor = Mockito.mock(DataAccessorIfc.class); + + ExpressionConfiguration configuration = + ExpressionConfiguration.builder().dataAccessorSupplier(() -> mockedDataAccessor).build(); + + assertThat(configuration.getDataAccessorSupplier().get()).isEqualTo(mockedDataAccessor); + } + + @Test + void testDataAccessorSupplierReturnsNewInstance() { + ExpressionConfiguration configuration = ExpressionConfiguration.defaultConfiguration(); + + DataAccessorIfc accessor1 = configuration.getDataAccessorSupplier().get(); + DataAccessorIfc accessor2 = configuration.getDataAccessorSupplier().get(); + + assertThat(accessor1).isNotEqualTo(accessor2); + } + + @Test + void testCustomConstants() { + Map constants = + new HashMap<>() { + { + put("A", new EvaluationValue("a")); + put("B", new EvaluationValue("b")); + } + }; + ExpressionConfiguration configuration = + ExpressionConfiguration.builder().defaultConstants(constants).build(); + + assertThat(configuration.getDefaultConstants()).containsAllEntriesOf(constants); + } + + @Test + void testArraysAllowed() { + ExpressionConfiguration configuration = + ExpressionConfiguration.builder().arraysAllowed(false).build(); + + assertThat(configuration.isArraysAllowed()).isFalse(); + } + + @Test + void testStructuresAllowed() { + ExpressionConfiguration configuration = + ExpressionConfiguration.builder().structuresAllowed(false).build(); + + assertThat(configuration.isStructuresAllowed()).isFalse(); + } + + @Test + void testImplicitMultiplicationAllowed() { + ExpressionConfiguration configuration = + ExpressionConfiguration.builder().implicitMultiplicationAllowed(false).build(); + + assertThat(configuration.isImplicitMultiplicationAllowed()).isFalse(); + } + + @Test + void testPowerOfPrecedence() { + ExpressionConfiguration configuration = + ExpressionConfiguration.builder() + .powerOfPrecedence(OperatorIfc.OPERATOR_PRECEDENCE_POWER_HIGHER) + .build(); + + assertThat(configuration.getPowerOfPrecedence()) + .isEqualTo(OperatorIfc.OPERATOR_PRECEDENCE_POWER_HIGHER); + } +} diff --git a/src/test/java/com/ezylang/evalex/config/MapBasedFunctionDictionaryTest.java b/src/test/java/com/ezylang/evalex/config/MapBasedFunctionDictionaryTest.java new file mode 100644 index 0000000..ba2b6a1 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/config/MapBasedFunctionDictionaryTest.java @@ -0,0 +1,62 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.functions.FunctionIfc; +import com.ezylang.evalex.functions.basic.MaxFunction; +import com.ezylang.evalex.functions.basic.MinFunction; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class MapBasedFunctionDictionaryTest { + + @Test + void testCreationOfFunctions() { + FunctionIfc min = new MinFunction(); + FunctionIfc max = new MaxFunction(); + + @SuppressWarnings({"unchecked", "varargs"}) + FunctionDictionaryIfc dictionary = + MapBasedFunctionDictionary.ofFunctions(Map.entry("min", min), Map.entry("max", max)); + + assertThat(dictionary.hasFunction("min")).isTrue(); + assertThat(dictionary.hasFunction("max")).isTrue(); + + assertThat(dictionary.getFunction("min")).isEqualTo(min); + assertThat(dictionary.getFunction("max")).isEqualTo(max); + + assertThat(dictionary.hasFunction("medium")).isFalse(); + } + + @Test + void testCaseInsensitivity() { + FunctionIfc min = new MinFunction(); + FunctionIfc max = new MaxFunction(); + + @SuppressWarnings({"unchecked", "varargs"}) + FunctionDictionaryIfc dictionary = + MapBasedFunctionDictionary.ofFunctions(Map.entry("Min", min), Map.entry("MAX", max)); + + assertThat(dictionary.hasFunction("min")).isTrue(); + assertThat(dictionary.hasFunction("MIN")).isTrue(); + assertThat(dictionary.hasFunction("Min")).isTrue(); + assertThat(dictionary.hasFunction("max")).isTrue(); + assertThat(dictionary.hasFunction("MAX")).isTrue(); + assertThat(dictionary.hasFunction("Max")).isTrue(); + } +} diff --git a/src/test/java/com/ezylang/evalex/config/MapBasedOperatorDictionaryTest.java b/src/test/java/com/ezylang/evalex/config/MapBasedOperatorDictionaryTest.java new file mode 100644 index 0000000..c920fae --- /dev/null +++ b/src/test/java/com/ezylang/evalex/config/MapBasedOperatorDictionaryTest.java @@ -0,0 +1,78 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.config.TestConfigurationProvider.PostfixQuestionOperator; +import com.ezylang.evalex.config.TestConfigurationProvider.PrefixPlusPlusOperator; +import com.ezylang.evalex.operators.OperatorIfc; +import com.ezylang.evalex.operators.arithmetic.InfixModuloOperator; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class MapBasedOperatorDictionaryTest { + + @Test + void testCreationOfOperators() { + OperatorIfc prefix = new PrefixPlusPlusOperator(); + OperatorIfc postfix = new PostfixQuestionOperator(); + OperatorIfc infix = new InfixModuloOperator(); + + @SuppressWarnings({"unchecked", "varargs"}) + OperatorDictionaryIfc dictionary = + MapBasedOperatorDictionary.ofOperators( + Map.entry("++", prefix), Map.entry("?", postfix), Map.entry("%", infix)); + + assertThat(dictionary.hasPrefixOperator("++")).isTrue(); + assertThat(dictionary.hasPostfixOperator("?")).isTrue(); + assertThat(dictionary.hasInfixOperator("%")).isTrue(); + + assertThat(dictionary.getPrefixOperator("++")).isEqualTo(prefix); + assertThat(dictionary.getPostfixOperator("?")).isEqualTo(postfix); + assertThat(dictionary.getInfixOperator("%")).isEqualTo(infix); + + assertThat(dictionary.hasPrefixOperator("A")).isFalse(); + assertThat(dictionary.hasPostfixOperator("B")).isFalse(); + assertThat(dictionary.hasInfixOperator("C")).isFalse(); + } + + @Test + void testCaseInsensitivity() { + OperatorIfc prefix = new PrefixPlusPlusOperator(); + OperatorIfc postfix = new PostfixQuestionOperator(); + OperatorIfc infix = new InfixModuloOperator(); + + @SuppressWarnings({"unchecked", "varargs"}) + OperatorDictionaryIfc dictionary = + MapBasedOperatorDictionary.ofOperators( + Map.entry("PlusPlus", prefix), + Map.entry("Question", postfix), + Map.entry("Percent", infix)); + + assertThat(dictionary.hasPrefixOperator("PlusPlus")).isTrue(); + assertThat(dictionary.hasPrefixOperator("plusplus")).isTrue(); + assertThat(dictionary.hasPrefixOperator("PLUSPLUS")).isTrue(); + + assertThat(dictionary.hasPostfixOperator("Question")).isTrue(); + assertThat(dictionary.hasPostfixOperator("question")).isTrue(); + assertThat(dictionary.hasPostfixOperator("QUESTION")).isTrue(); + + assertThat(dictionary.hasInfixOperator("Percent")).isTrue(); + assertThat(dictionary.hasInfixOperator("percent")).isTrue(); + assertThat(dictionary.hasInfixOperator("PERCENT")).isTrue(); + } +} diff --git a/src/test/java/com/ezylang/evalex/config/TestConfigurationProvider.java b/src/test/java/com/ezylang/evalex/config/TestConfigurationProvider.java new file mode 100644 index 0000000..6be6313 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/config/TestConfigurationProvider.java @@ -0,0 +1,80 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.config; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.PostfixOperator; +import com.ezylang.evalex.operators.PrefixOperator; +import com.ezylang.evalex.parser.Token; +import java.math.BigDecimal; +import java.util.Map; + +public class TestConfigurationProvider { + + public static final ExpressionConfiguration StandardConfigurationWithAdditionalTestOperators = + ExpressionConfiguration.defaultConfiguration() + .withAdditionalOperators( + Map.entry("++", new PrefixPlusPlusOperator()), + Map.entry("++", new PostfixPlusPlusOperator()), + Map.entry("?", new PostfixQuestionOperator())) + .withAdditionalFunctions(Map.entry("TEST", new DummyFunction())); + + @FunctionParameter(name = "input", isVarArg = true) + public static class DummyFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + // dummy implementation + return new EvaluationValue("OK"); + } + } + + @PrefixOperator(leftAssociative = false) + public static class PrefixPlusPlusOperator extends AbstractOperator { + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + // dummy implementation + EvaluationValue operand = operands[0]; + return new EvaluationValue(operand.getNumberValue().add(BigDecimal.ONE)); + } + } + + @PostfixOperator() + public static class PostfixPlusPlusOperator extends AbstractOperator { + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + // dummy implementation + EvaluationValue operand = operands[0]; + return new EvaluationValue(operand.getNumberValue().add(BigDecimal.ONE)); + } + } + + @PostfixOperator(leftAssociative = false) + public static class PostfixQuestionOperator extends AbstractOperator { + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + // dummy implementation + return new EvaluationValue("?"); + } + } +} diff --git a/src/test/java/com/ezylang/evalex/data/EvaluationValueTest.java b/src/test/java/com/ezylang/evalex/data/EvaluationValueTest.java new file mode 100644 index 0000000..c67259d --- /dev/null +++ b/src/test/java/com/ezylang/evalex/data/EvaluationValueTest.java @@ -0,0 +1,257 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.parser.ASTNode; +import com.ezylang.evalex.parser.Token; +import com.ezylang.evalex.parser.Token.TokenType; +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class EvaluationValueTest { + + @Test + void testUnsupportedDataType() { + assertThatThrownBy(() -> new EvaluationValue(Locale.FRANCE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported data type 'java.util.Locale'"); + } + + @Test + void testString() { + EvaluationValue value = new EvaluationValue("Hello World"); + + assertThat(value.isStringValue()).isTrue(); + assertDataIsCorrect(value, "Hello World", BigDecimal.ZERO, false, String.class); + } + + @Test + void testStringBuilder() { + EvaluationValue value = new EvaluationValue(new StringBuilder("Hello StringBuilder World")); + + assertThat(value.isStringValue()).isTrue(); + assertDataIsCorrect(value, "Hello StringBuilder World", BigDecimal.ZERO, false, String.class); + } + + @Test + void tesSCharacter() { + EvaluationValue value = new EvaluationValue('a'); + + assertThat(value.isStringValue()).isTrue(); + assertDataIsCorrect(value, "a", BigDecimal.ZERO, false, String.class); + } + + @Test + void testBooleanTrue() { + EvaluationValue value = new EvaluationValue(true); + + assertThat(value.isBooleanValue()).isTrue(); + assertDataIsCorrect(value, "true", BigDecimal.ONE, true, Boolean.class); + } + + @Test + void testBooleanFalse() { + EvaluationValue value = new EvaluationValue(false); + + assertThat(value.isBooleanValue()).isTrue(); + assertDataIsCorrect(value, "false", BigDecimal.ZERO, false, Boolean.class); + } + + @Test + void testBooleanString() { + EvaluationValue value = new EvaluationValue("true"); + + assertThat(value.isStringValue()).isTrue(); + assertDataIsCorrect(value, "true", BigDecimal.ONE, true, String.class); + } + + @Test + void testBooleanNumberZero() { + EvaluationValue value = new EvaluationValue(BigDecimal.ZERO); + + assertThat(value.isNumberValue()).isTrue(); + assertDataIsCorrect(value, "0", BigDecimal.ZERO, false, BigDecimal.class); + } + + @Test + void testBigDecimal() { + EvaluationValue value = new EvaluationValue(new BigDecimal("123.5")); + + assertThat(value.isNumberValue()).isTrue(); + assertDataIsCorrect(value, "123.5", new BigDecimal("123.5"), true, BigDecimal.class); + } + + @Test + void testFloat() { + EvaluationValue value = new EvaluationValue((float) 4.5); + + assertThat(value.isNumberValue()).isTrue(); + assertDataIsCorrect(value, "4.5", BigDecimal.valueOf((float) 4.5), true, BigDecimal.class); + } + + @Test + void testDouble() { + EvaluationValue value = new EvaluationValue(8.5); + + assertThat(value.isNumberValue()).isTrue(); + assertDataIsCorrect(value, "8.5", BigDecimal.valueOf(8.5), true, BigDecimal.class); + } + + @Test + void testLong() { + EvaluationValue value = new EvaluationValue(6L); + + assertThat(value.isNumberValue()).isTrue(); + assertDataIsCorrect(value, "6", new BigDecimal(6), true, BigDecimal.class); + } + + @Test + void testInteger() { + EvaluationValue value = new EvaluationValue(5); + + assertThat(value.isNumberValue()).isTrue(); + assertDataIsCorrect(value, "5", new BigDecimal(5), true, BigDecimal.class); + } + + @Test + void testShort() { + EvaluationValue value = new EvaluationValue((short) 4); + + assertThat(value.isNumberValue()).isTrue(); + assertDataIsCorrect(value, "4", new BigDecimal(4), true, BigDecimal.class); + } + + @Test + void testByte() { + EvaluationValue value = new EvaluationValue((byte) 3); + + assertThat(value.isNumberValue()).isTrue(); + assertDataIsCorrect(value, "3", new BigDecimal(3), true, BigDecimal.class); + } + + @Test + void testArray() { + EvaluationValue value = + new EvaluationValue(Arrays.asList(new BigDecimal(1), new BigDecimal(2))); + + assertThat(value.isArrayValue()).isTrue(); + + assertThat(value.getArrayValue()).hasSize(2); + assertThat(value.getArrayValue().get(0).getStringValue()).isEqualTo("1"); + assertThat(value.getArrayValue().get(1).getStringValue()).isEqualTo("2"); + + assertThat(value.getValue()).isInstanceOf(List.class); + } + + @Test + void testArrayEmpty() { + EvaluationValue value = new EvaluationValue(new BigDecimal(1)); + + assertThat(value.getArrayValue()).isEmpty(); + } + + @Test + void testStructure() { + Map structure = new HashMap<>(); + structure.put("a", "Hello"); + structure.put("b", new BigDecimal(99)); + EvaluationValue value = new EvaluationValue(structure); + + assertThat(value.isStructureValue()).isTrue(); + + assertThat(value.getStructureValue()).hasSize(2); + assertThat(value.getStructureValue().get("a").getStringValue()).isEqualTo("Hello"); + assertThat(value.getStructureValue().get("b").getStringValue()).isEqualTo("99"); + + assertThat(value.getValue()).isInstanceOf(Map.class); + } + + @Test + void testStructureEmpty() { + EvaluationValue value = new EvaluationValue(new BigDecimal(1)); + + assertThat(value.getStructureValue()).isEmpty(); + } + + @Test + void testExpressionNode() { + ASTNode node = new ASTNode(new Token(1, "a", TokenType.VARIABLE_OR_CONSTANT)); + EvaluationValue value = new EvaluationValue(node); + + assertThat(value.isExpressionNode()).isTrue(); + + assertDataIsCorrect( + value, + "ASTNode(parameters=[], token=Token(startPosition=1, value=a, type=VARIABLE_OR_CONSTANT))", + BigDecimal.ZERO, + false, + ASTNode.class); + } + + @Test + void testNumberOfString() { + EvaluationValue value = EvaluationValue.numberOfString("123.987", MathContext.DECIMAL128); + + assertThat(value.isNumberValue()).isTrue(); + assertThat(value.getNumberValue()).isEqualTo(new BigDecimal("123.987", MathContext.DECIMAL128)); + assertThat(value).hasToString("EvaluationValue(value=123.987, dataType=NUMBER)"); + } + + @Test + void testCompare() { + assertThat(new EvaluationValue("Hello")).isEqualByComparingTo(new EvaluationValue("Hello")); + assertThat(new EvaluationValue(123)).isEqualByComparingTo(new EvaluationValue(123)); + assertThat(new EvaluationValue(true)).isEqualByComparingTo(new EvaluationValue(true)); + + assertThat(new EvaluationValue("Hello")).isGreaterThan(new EvaluationValue("Hell")); + assertThat(new EvaluationValue(124)).isGreaterThan(new EvaluationValue(123)); + assertThat(new EvaluationValue(true)).isGreaterThan(new EvaluationValue(false)); + + assertThat(new EvaluationValue("Hell")).isLessThan(new EvaluationValue("Hello")); + assertThat(new EvaluationValue(123)).isLessThan(new EvaluationValue(124)); + assertThat(new EvaluationValue(false)).isLessThan(new EvaluationValue(true)); + } + + @Test + void testDoubleMathContext() { + assertThat(new EvaluationValue(3.9876, MathContext.DECIMAL64).getNumberValue()) + .isEqualByComparingTo("3.9876"); + + assertThat(new EvaluationValue(3.9876, new MathContext(3)).getNumberValue()) + .isEqualByComparingTo("3.99"); + } + + private void assertDataIsCorrect( + EvaluationValue value, + String stringValue, + BigDecimal numberValue, + Boolean booleanValue, + Class valueInstance) { + assertThat(value.getStringValue()).isEqualTo(stringValue); + assertThat(value.getNumberValue()).isEqualTo(numberValue); + assertThat(value.getBooleanValue()).isEqualTo(booleanValue); + assertThat(value.getValue()).isInstanceOf(valueInstance); + } +} diff --git a/src/test/java/com/ezylang/evalex/data/MapBasedDataAccessorTest.java b/src/test/java/com/ezylang/evalex/data/MapBasedDataAccessorTest.java new file mode 100644 index 0000000..086e9dc --- /dev/null +++ b/src/test/java/com/ezylang/evalex/data/MapBasedDataAccessorTest.java @@ -0,0 +1,53 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.data; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; +import org.junit.jupiter.api.Test; + +class MapBasedDataAccessorTest { + + @Test + void testSetGetData() { + DataAccessorIfc dataAccessor = new MapBasedDataAccessor(); + + EvaluationValue num = new EvaluationValue(new BigDecimal("123")); + EvaluationValue string = new EvaluationValue("hello"); + EvaluationValue bool = new EvaluationValue(true); + + dataAccessor.setData("num", num); + dataAccessor.setData("string", string); + dataAccessor.setData("bool", bool); + + assertThat(dataAccessor.getData("num")).isEqualTo(num); + assertThat(dataAccessor.getData("string")).isEqualTo(string); + assertThat(dataAccessor.getData("bool")).isEqualTo(bool); + } + + @Test + void testCaseInsensitivity() { + DataAccessorIfc dataAccessor = new MapBasedDataAccessor(); + + EvaluationValue num = new EvaluationValue(new BigDecimal("123")); + dataAccessor.setData("Hello", num); + + assertThat(dataAccessor.getData("Hello")).isEqualTo(num); + assertThat(dataAccessor.getData("hello")).isEqualTo(num); + assertThat(dataAccessor.getData("HELLO")).isEqualTo(num); + } +} diff --git a/src/test/java/com/ezylang/evalex/functions/FunctionParameterDefinitionTest.java b/src/test/java/com/ezylang/evalex/functions/FunctionParameterDefinitionTest.java new file mode 100644 index 0000000..7d047e1 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/functions/FunctionParameterDefinitionTest.java @@ -0,0 +1,46 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class FunctionParameterDefinitionTest { + + @Test + void testCreation() { + FunctionParameterDefinition definition = + FunctionParameterDefinition.builder() + .name("name") + .isVarArg(true) + .isLazy(true) + .nonZero(true) + .nonNegative(true) + .build(); + + assertThat(definition.getName()).isEqualTo("name"); + assertThat(definition.isVarArg()).isTrue(); + assertThat(definition.isLazy()).isTrue(); + assertThat(definition.isNonZero()).isTrue(); + assertThat(definition.isNonNegative()).isTrue(); + + assertThat(definition) + .hasToString( + "FunctionParameterDefinition(name=name, isVarArg=true, isLazy=true, nonZero=true," + + " nonNegative=true)"); + } +} diff --git a/src/test/java/com/ezylang/evalex/functions/FunctionTest.java b/src/test/java/com/ezylang/evalex/functions/FunctionTest.java new file mode 100644 index 0000000..2ef0a17 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/functions/FunctionTest.java @@ -0,0 +1,84 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.Token; +import org.junit.jupiter.api.Test; + +class FunctionTest { + + @Test + void testParameterDefinition() { + FunctionIfc function = new CorrectFunctionDefinitionFunction(); + + assertThat(function.getFunctionParameterDefinitions().get(0).getName()).isEqualTo("default"); + assertThat(function.getFunctionParameterDefinitions().get(0).isLazy()).isFalse(); + assertThat(function.getFunctionParameterDefinitions().get(0).isVarArg()).isFalse(); + + assertThat(function.getFunctionParameterDefinitions().get(1).getName()).isEqualTo("lazy"); + assertThat(function.getFunctionParameterDefinitions().get(1).isLazy()).isTrue(); + assertThat(function.getFunctionParameterDefinitions().get(1).isVarArg()).isFalse(); + + assertThat(function.getFunctionParameterDefinitions().get(2).getName()).isEqualTo("vararg"); + assertThat(function.getFunctionParameterDefinitions().get(2).isLazy()).isFalse(); + assertThat(function.getFunctionParameterDefinitions().get(2).isVarArg()).isTrue(); + } + + @Test + void testParameterIsLazy() { + FunctionIfc function = new CorrectFunctionDefinitionFunction(); + + assertThat(function.isParameterLazy(0)).isFalse(); + assertThat(function.isParameterLazy(1)).isTrue(); + assertThat(function.isParameterLazy(2)).isFalse(); + assertThat(function.isParameterLazy(3)).isFalse(); + assertThat(function.isParameterLazy(4)).isFalse(); + } + + @Test + void testVarargNotAllowed() { + assertThatThrownBy(WrongVarargFunctionDefinitionFunction::new) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Only last parameter may be defined as variable argument"); + } + + @FunctionParameter(name = "default") + @FunctionParameter(name = "lazy", isLazy = true) + @FunctionParameter(name = "vararg", isVarArg = true) + private static class CorrectFunctionDefinitionFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + return new EvaluationValue("OK"); + } + } + + @FunctionParameter(name = "default") + @FunctionParameter(name = "vararg", isVarArg = true) + @FunctionParameter(name = "another") + private static class WrongVarargFunctionDefinitionFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + return new EvaluationValue("OK"); + } + } +} diff --git a/src/test/java/com/ezylang/evalex/functions/basic/BasicFunctionsTest.java b/src/test/java/com/ezylang/evalex/functions/basic/BasicFunctionsTest.java new file mode 100644 index 0000000..646f368 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/functions/basic/BasicFunctionsTest.java @@ -0,0 +1,297 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.BaseEvaluationTest; +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.ParseException; +import java.math.MathContext; +import java.math.RoundingMode; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class BasicFunctionsTest extends BaseEvaluationTest { + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "FACT(0) : 1", + "FACT(1) : 1", + "FACT(2) : 2", + "FACT(3) : 6", + "FACT(5) : 120", + "FACT(10) : 3628800", + "FACT(20) : 2432902008176640000" + }) + void testFactorial(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "IF(1, 4/2, 4/0) : 2", + "IF(1, 4/IF(0, 5/0, 2*2), 4/0) : 1", + "IF(1, 6/IF(0, 5/0, 2*IF(1, 3, 6/0)), 4/0) : 1" + }) + void testIf(String expression, String expectedResult) throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "MAX(99) : 99", + "MAX(2,1) : 2", + "MAX(1,9,-5,6,3,7) : 9", + "MAX(17,88,77,66,609,1567,1876534) : 1876534" + }) + void testMax(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "MIN(99) : 99", + "MIN(2,1) : 1", + "MIN(1,9,-5,6,3,7) : -5", + "MIN(17,88,77,66,609,1567,1876534) : 17" + }) + void testMin(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "ROUND(1.1,0) : 1", + "ROUND(1.5,0) : 2", + "ROUND(2.34,1) : 2.3", + "ROUND(2.35,1) : 2.4", + "ROUND(2.323789,2) : 2.32", + "ROUND(2.324789,2) : 2.32" + }) + void testRoundHalfEven(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "ROUND(1.1,0) : 2", + "ROUND(1.5,0) : 2", + "ROUND(2.34,1) : 2.4", + "ROUND(2.35,1) : 2.4", + "ROUND(2.323789,2) : 2.33", + "ROUND(2.324789,2) : 2.33" + }) + void testRoundUp(String expression, String expectedResult) + throws EvaluationException, ParseException { + ExpressionConfiguration configuration = + ExpressionConfiguration.builder().mathContext(new MathContext(32, RoundingMode.UP)).build(); + assertExpressionHasExpectedResult(expression, expectedResult, configuration); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "SUM(1) : 1", + "SUM(1,2,3,4) : 10", + "SUM(1,-1) : 0", + "SUM(1,10,100,1000,10000) : 11111", + "SUM(1,2,3,-3,-2,5) : 6" + }) + void testSum(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "SQRT(0) : 0", + "SQRT(1) : 1", + "SQRT(2) : 1.41421356237309504880168872420969807856967187537694807317667973799073", + "SQRT(4) : 2", + "SQRT(5) : 2.23606797749978969640917366873127623544061835961152572427089724541052", + "SQRT(10) : 3.16227766016837933199889354443271853371955513932521682685750485279259", + "SQRT(236769) : 486.58914907753543122473972072155030396245230523850016876894122736411182" + }) + void testSqrt(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testSqrtNegative() { + assertThatThrownBy(() -> new Expression("SQRT(-1)").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Parameter must not be negative"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "NOT(0) : true", + "NOT(1) : false", + "NOT(20) : false", + "NOT(\"true\") : false", + "NOT(\"false\") : true", + "NOT(2-4/2) : true", + }) + void testNot(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testRandom() throws EvaluationException, ParseException { + EvaluationValue r1 = new Expression("RANDOM()").evaluate(); + EvaluationValue r2 = new Expression("RANDOM()").evaluate(); + + assertThat(r1).isNotEqualByComparingTo(r2); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "ABS(0) : 0", + "ABS(1) : 1", + "ABS(-1) : 1", + "ABS(20) : 20", + "ABS(-20) : 20", + "ABS(2.12345) : 2.12345", + "ABS(-2.12345) : 2.12345" + }) + void testAbs(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "FLOOR(0) : 0", + "FLOOR(1) : 1", + "FLOOR(-1) : -1", + "FLOOR(20) : 20", + "FLOOR(-20) : -20", + "FLOOR(2.12345) : 2", + "FLOOR(-2.12345) : -3", + "FLOOR(-2.97345) : -3" + }) + void testFloor(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "CEILING(0) : 0", + "CEILING(1) : 1", + "CEILING(-1) : -1", + "CEILING(20) : 20", + "CEILING(-20) : -20", + "CEILING(2.12345) : 3", + "CEILING(-2.12345) : -2", + "CEILING(-2.97345) : -2" + }) + void testCeiling(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "LOG(1) : 0", + "LOG(10) : 2.302585092994046", + "LOG(2.12345) : 0.7530421244614831", + "LOG(1567) : 7.356918242356021" + }) + void testLog(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testLogNegative() { + assertThatThrownBy(() -> new Expression("LOG(-1)").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Parameter must not be negative"); + } + + @Test + void testLogZero() { + assertThatThrownBy(() -> new Expression("LOG(0)").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Parameter must not be zero"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "LOG10(1) : 0", + "LOG10(10) : 1", + "LOG10(2.12345) : 0.3270420392943239", + "LOG10(1567) : 3.1950689964685903" + }) + void testLog10(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testLog10Negative() { + assertThatThrownBy(() -> new Expression("LOG10(-1)").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Parameter must not be negative"); + } + + @Test + void testLog10Zero() { + assertThatThrownBy(() -> new Expression("LOG10(0)").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Parameter must not be zero"); + } +} diff --git a/src/test/java/com/ezylang/evalex/functions/string/StringFunctionsTest.java b/src/test/java/com/ezylang/evalex/functions/string/StringFunctionsTest.java new file mode 100644 index 0000000..c622749 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/functions/string/StringFunctionsTest.java @@ -0,0 +1,74 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.BaseEvaluationTest; +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.parser.ParseException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class StringFunctionsTest extends BaseEvaluationTest { + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "STR_UPPER(\"\") : ''", + "STR_UPPER(\"a\") : A", + "STR_UPPER(\"A\") : A", + "STR_UPPER(\"AbCdEf\") : ABCDEF", + "STR_UPPER(\"A1b3C4/?\") : A1B3C4/?", + "STR_UPPER(\"äöüß\") : ÄÖÜSS" + }) + void testUpper(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "STR_LOWER(\"\") : ''", + "STR_LOWER(\"A\") : a", + "STR_LOWER(\"a\") : a", + "STR_LOWER(\"AbCdEf\") : abcdef", + "STR_LOWER(\"A1b3C4/?\") : a1b3c4/?", + "STR_LOWER(\"ÄÖÜSS\") : äöüss" + }) + void testLower(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "STR_CONTAINS(\"\", \"\") : true", + "STR_CONTAINS(\"a\", \"a\") : true", + "STR_CONTAINS(\"Hello World\", \"Wor\") : true", + "STR_CONTAINS(\"What a world\", \"what\") : true", + "STR_CONTAINS(\"What a world\", \"a world\") : true", + "STR_CONTAINS(\"What a world\", \"moon\") : false", + "STR_CONTAINS(\"\", \"text\") : false" + }) + void testContains(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } +} diff --git a/src/test/java/com/ezylang/evalex/functions/trigonometric/TrigonometricFunctionsTest.java b/src/test/java/com/ezylang/evalex/functions/trigonometric/TrigonometricFunctionsTest.java new file mode 100644 index 0000000..c203920 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/functions/trigonometric/TrigonometricFunctionsTest.java @@ -0,0 +1,566 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.trigonometric; + +import com.ezylang.evalex.BaseEvaluationTest; +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.parser.ParseException; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class TrigonometricFunctionsTest extends BaseEvaluationTest { + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "DEG(0) : 0", + "DEG(1) : 57.29577951308232", + "DEG(90) : 5156.620156177409", + "DEG(-90) : -5156.620156177409" + }) + void testDeg(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "RAD(0) : 0", + "RAD(1) : 0.017453292519943295", + "RAD(45) : 0.7853981633974483", + "RAD(50) : 0.8726646259971648", + "RAD(90) : 1.5707963267948966", + "RAD(-90) : -1.5707963267948966" + }) + void testRad(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = {"SIN(0) : 0", "SIN(1) : 0.01745240643728351", "SIN(90) : 1", "SIN(-90) : -1"}) + void testSin(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "COS(0) : 1", + "COS(1) : 0.9998476951563913", + "COS(19) : 0.9455185755993168", + "COS(-19) : 0.9455185755993168" + }) + void testCos(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "TAN(0) : 0", + "TAN(1) : 0.017455064928217585", + "TAN(19) : 0.34432761328966527", + "TAN(-19) : -0.34432761328966527" + }) + void testTan(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "SINR(0) : 0", + "SINR(1) : 0.8414709848078965", + "SINR(90) : 0.8939966636005579", + "SINR(-90) : -0.8939966636005579" + }) + void testSinR(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "COSR(0) : 1", + "COSR(1) : 0.5403023058681398", + "COSR(19) : 0.9887046181866692", + "COSR(-19) : 0.9887046181866692" + }) + void testCosR(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "TANR(0) : 0", + "TANR(1) : 1.5574077246549023", + "TANR(19) : 0.15158947061240008", + "TANR(-19) : -0.15158947061240008" + }) + void testTanR(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "COT(1) : 57.28996163075943", + "COT(19) : 2.9042108776758226", + "COT(-19) : -2.9042108776758226" + }) + void testCoTan(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testCoTanThrowsException() { + Assertions.assertThatThrownBy(() -> new Expression("COT(0)").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Parameter must not be zero"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "COTH(1) : 1.3130352854993315", + "COTH(5) : 1.0000908039820193", + "COTH(-5) : -1.0000908039820193" + }) + void testCotH(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testCotHThrowsException() { + Assertions.assertThatThrownBy(() -> new Expression("COTH(0)").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Parameter must not be zero"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "COTR(1) : 0.6420926159343306", + "COTR(19) : 6.596764247280111", + "COTR(-19) : -6.596764247280111" + }) + void testCoTanR(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testCoTanRThrowsException() { + Assertions.assertThatThrownBy(() -> new Expression("COTR(0)").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Parameter must not be zero"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "SEC(1) : 1.0001523280439077", + "SEC(19) : 1.0576206811866706", + "SEC(-19) : 1.0576206811866706" + }) + void testSec(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testSecThrowsException() { + Assertions.assertThatThrownBy(() -> new Expression("SEC(0)").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Parameter must not be zero"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "SECH(1) : 0.6480542736638853", + "SECH(19) : 0.000000011205592875074534", + "SECH(-19) : 0.000000011205592875074534" + }) + void testSecH(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testSecHThrowsException() { + Assertions.assertThatThrownBy(() -> new Expression("SECH(0)").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Parameter must not be zero"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "SECR(1) : 1.8508157176809255", + "SECR(19) : 1.01142442505634", + "SECR(-19) : 1.01142442505634" + }) + void testSecR(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testSecRThrowsException() { + Assertions.assertThatThrownBy(() -> new Expression("SECR(0)").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Parameter must not be zero"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "CSC(1) : 57.298688498550185", + "CSC(19) : 3.0715534867572423", + "CSC(-19) : -3.0715534867572423" + }) + void testCSC(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testCscThrowsException() { + Assertions.assertThatThrownBy(() -> new Expression("CSC(0)").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Parameter must not be zero"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "CSCH(1) : 0.8509181282393216", + "CSCH(19) : 0.000000011205592875074534", + "CSCH(-19) : -0.000000011205592875074534" + }) + void testCSCH(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testCscHThrowsException() { + Assertions.assertThatThrownBy(() -> new Expression("CSCH(0)").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Parameter must not be zero"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "CSCR(1) : 1.1883951057781212", + "CSCR(19) : 6.672128486037505", + "CSCR(-19) : -6.672128486037505" + }) + void testCSCR(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testCscRThrowsException() { + Assertions.assertThatThrownBy(() -> new Expression("CSCR(0)").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Parameter must not be zero"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "ACOS(0) : 90", + "ACOS(1) : 0", + "ACOS(-1) : 180", + }) + void testAcos(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "ACOSH(1) : 0", + "ACOSH(2) : 1.3169578969248166", + "ACOSH(3) : 1.762747174039086", + }) + void testAcosH(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @ValueSource(doubles = {-1, -0.5, 0, 0.5, 0.9}) + void testAcosHThrowsException(double d) { + Assertions.assertThatThrownBy(() -> new Expression("ACOSH(x)").with("x", d).evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Value must be greater or equal to one"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "ACOSR(0) : 1.5707963267948966", + "ACOSR(1) : 0", + "ACOSR(-1) : 3.141592653589793", + }) + void testAcosR(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "ACOT(1) : 45", + "ACOT(-1) : -45", + }) + void testAcot(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testAcotThrowsException() { + Assertions.assertThatThrownBy(() -> new Expression("ACOT(0)").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Parameter must not be zero"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "ACOTR(1) : 0.7853981633974483", + "ACOTR(-1) : -0.7853981633974483", + }) + void testAcotR(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testAcotRThrowsException() { + Assertions.assertThatThrownBy(() -> new Expression("ACOTR(0)").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Parameter must not be zero"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "ASIN(0) : 0", + "ASIN(1) : 90", + "ASIN(-1) : -90", + }) + void testAsin(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "ASINH(0) : 0", + "ASINH(1) : 0.8813735870195429", + "ASINH(-1) : -0.8813735870195428", + }) + void testAsinH(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "ASINR(0) : 0", + "ASINR(1) : 1.5707963267948966", + "ASINR(-1) : -1.5707963267948966", + }) + void testAsinR(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "ATAN(0) : 0", + "ATAN(1) : 45", + "ATAN(-1) : -45", + }) + void testAtan(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "ATANH(0) : 0", + "ATANH(0.9) : 1.4722194895832204", + "ATANH(-0.9) : -1.4722194895832204", + }) + void testAtanH(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @ValueSource(doubles = {-1.1, -1.0, 1.0, 1.1}) + void testAtanHThrowsException(double d) { + Assertions.assertThatThrownBy(() -> new Expression("ATANH(x)").with("x", d).evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Absolute value must be less than 1"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "ATANR(0) : 0", + "ATANR(1) : 0.7853981633974483", + "ATANR(-1) : -0.7853981633974483", + }) + void testAtanR(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "ATAN2(0,0) : 0", + "ATAN2(0,1) : 0", + "ATAN2(0,-1) : 180", + "ATAN2(1,0) : 90", + "ATAN2(1,1) : 45", + "ATAN2(1,-1) : 135", + "ATAN2(-1,0) : -90", + "ATAN2(-1,1) : -45", + "ATAN2(-1,-1) : -135", + }) + void testAtan2(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "ATAN2R(0,0) : 0", + "ATAN2R(0,1) : 0", + "ATAN2R(0,-1) : 3.141592653589793", + "ATAN2R(1,0) : 1.5707963267948966", + "ATAN2R(1,1) : 0.7853981633974483", + "ATAN2R(1,-1) : 2.356194490192345", + "ATAN2R(-1,0) : -1.5707963267948966", + "ATAN2R(-1,1) : -0.7853981633974483", + "ATAN2R(-1,-1) : -2.356194490192345", + }) + void testAtan2R(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "COSH(0) : 1", + "COSH(1) : 1.543080634815244", + "COSH(-1) : 1.543080634815244", + }) + void testCosH(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "SINH(0) : 0", + "SINH(1) : 1.1752011936438014", + "SINH(-1) : -1.1752011936438014", + }) + void testSinH(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "TANH(0) : 0", + "TANH(1) : 0.7615941559557649", + "TANH(-1) : -0.7615941559557649", + }) + void testTanH(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } +} diff --git a/src/test/java/com/ezylang/evalex/operators/OperatorTest.java b/src/test/java/com/ezylang/evalex/operators/OperatorTest.java new file mode 100644 index 0000000..317b993 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/operators/OperatorTest.java @@ -0,0 +1,93 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_MULTIPLICATIVE; +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_UNARY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.Token; +import org.junit.jupiter.api.Test; + +class OperatorTest { + + @Test + void testPrefixOperator() { + OperatorIfc operator = new CorrectPrefixOperator(); + + assertThat(operator.getPrecedence()).isEqualTo(OPERATOR_PRECEDENCE_UNARY); + assertThat(operator.isLeftAssociative()).isFalse(); + + assertThat(operator.isPrefix()).isTrue(); + assertThat(operator.isPostfix()).isFalse(); + assertThat(operator.isInfix()).isFalse(); + } + + @Test + void testPostfixOperator() { + OperatorIfc operator = new CorrectPostfixOperator(); + + assertThat(operator.getPrecedence()).isEqualTo(88); + assertThat(operator.isLeftAssociative()).isTrue(); + + assertThat(operator.isPrefix()).isFalse(); + assertThat(operator.isPostfix()).isTrue(); + assertThat(operator.isInfix()).isFalse(); + } + + @Test + void testInfixOperator() { + OperatorIfc operator = new CorrectInfixOperator(); + + assertThat(operator.getPrecedence()).isEqualTo(OPERATOR_PRECEDENCE_MULTIPLICATIVE); + assertThat(operator.isLeftAssociative()).isTrue(); + + assertThat(operator.isPrefix()).isFalse(); + assertThat(operator.isPostfix()).isFalse(); + assertThat(operator.isInfix()).isTrue(); + } + + @Test + void testThrowsFunctionParameterAnnotationNotFoundException() { + + assertThatThrownBy(DummyAnnotationOperator::new) + .isInstanceOf(OperatorAnnotationNotFoundException.class) + .hasMessage( + "Operator annotation for" + + " 'com.ezylang.evalex.operators.OperatorTest$DummyAnnotationOperator' not" + + " found"); + } + + @PrefixOperator(leftAssociative = false) + private static class CorrectPrefixOperator extends DummyAnnotationOperator {} + + @PostfixOperator(precedence = 88) + private static class CorrectPostfixOperator extends DummyAnnotationOperator {} + + @InfixOperator(precedence = OPERATOR_PRECEDENCE_MULTIPLICATIVE) + private static class CorrectInfixOperator extends DummyAnnotationOperator {} + + private static class DummyAnnotationOperator extends AbstractOperator { + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return new EvaluationValue("OK"); + } + } +} diff --git a/src/test/java/com/ezylang/evalex/operators/arithmetic/ArithmeticOperatorsTest.java b/src/test/java/com/ezylang/evalex/operators/arithmetic/ArithmeticOperatorsTest.java new file mode 100644 index 0000000..b57beff --- /dev/null +++ b/src/test/java/com/ezylang/evalex/operators/arithmetic/ArithmeticOperatorsTest.java @@ -0,0 +1,206 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.BaseEvaluationTest; +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.parser.ParseException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class ArithmeticOperatorsTest extends BaseEvaluationTest { + + @ParameterizedTest + @ValueSource( + strings = { + "3/\"string\"", + "\"string\"/3", + "3-\"string\"", + "\"string\"-3", + "3%\"string\"", + "\"string\"%3", + "3*\"string\"", + "\"string\"*3", + "3^\"string\"", + "\"string\"^3", + "-\"string\"", + "+\"string\"" + }) + void testUnsupportedDataType(String expression) { + assertThatThrownBy(() -> assertExpressionHasExpectedResult(expression, "0")) + .isInstanceOf(EvaluationException.class) + .hasMessage("Unsupported data types in operation"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "1/1 : 1", + "1/2 : 0.5", + "1/3 : 0.33333333333333333333333333333333333333333333333333333333333333333333", + "2/8 : 0.25", + "8/2 : 4", + "426376/478 : 892" + }) + void testInfixDivision(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testInfixDivisionByZero() { + assertThatThrownBy(() -> assertExpressionHasExpectedResult("3/0", "0")) + .isInstanceOf(EvaluationException.class) + .hasMessage("Division by zero"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "1-1 : 0", + "1-0 : 1", + "0-1 : -1", + "22-22 : 0", + "2.8-2.4 : 0.4", + "1-89282 : -89281", + "1.54321-1.5432 : 0.00001" + }) + void testInfixMinus(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "1%1 : 0", + "0%1 : 0", + "22%2 : 0", + "2.8%2 : 0.8", + "1%89282 : 1", + "154321%7 : 6", + "848484%7373 : 589" + }) + void testInfixModulo(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testInfixModuloByZero() { + assertThatThrownBy(() -> assertExpressionHasExpectedResult("3%0", "0")) + .isInstanceOf(EvaluationException.class) + .hasMessage("Division by zero"); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "0*0 : 0", + "0*1 : 0", + "1*0 : 0", + "1*1 : 1", + "2*2 : 4", + "17*76 : 1292", + "1.5*2 : 3", + "2.125*4 : 8.5" + }) + void testInfixMultiplication(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "0+0 : 0", + "0+1 : 1", + "1+0 : 1", + "1+1 : 2", + "2+3 : 5", + "17+76 : 93", + "1.5+2 : 3.5", + "2.125+4.125 : 6.25" + }) + void testInfixPlusNumbers(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "\"hello\"+\" world\" : hello world", + "77+\"hello\" : 77hello", + "\"hello\"+77 : hello77", + "\"1\"+\"1\" : 11", + "1+\"1\" : 11", + "\"1\"+1 : 11" + }) + void testInfixPlusStrings(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "0^0 : 1", + "0^2 : 0", + "2^0 : 1", + "1^1 : 1", + "1^2 : 1", + "2^2 : 4", + "4^4 : 256", + "2.3^2.2 : 6.248866394748043436", + "2^-3 : 0.125", + "-2^-3 : -0.125", + "-2^-3 : -0.125" + }) + void testInfixPower(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = {"-0 : 0", "-1 : -1", "-2.5 : -2.5", "-987.654 : -987.654"}) + void testPrefixMinus(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = {"+0 : 0", "+1 : 1", "+2.5 : 2.5", "+987.654 : 987.654"}) + void testPrefixPlus(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } +} diff --git a/src/test/java/com/ezylang/evalex/operators/booleans/InfixAndOperatorTest.java b/src/test/java/com/ezylang/evalex/operators/booleans/InfixAndOperatorTest.java new file mode 100644 index 0000000..7e926ec --- /dev/null +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixAndOperatorTest.java @@ -0,0 +1,44 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import com.ezylang.evalex.BaseEvaluationTest; +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.parser.ParseException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class InfixAndOperatorTest extends BaseEvaluationTest { + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "1&&1 : true", + "1&&2 : true", + "0&&1 : false", + "22&&33 : true", + "\"true\"&&\"true\" : true", + "\"true\"&&\"false\" : false", + "\"false\"&&\"false\" : false", + "(1==1)&&(2==2) : true", + "(5>4)&&(4<6) :true" + }) + void testInfixLessLiterals(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } +} diff --git a/src/test/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperatorTest.java b/src/test/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperatorTest.java new file mode 100644 index 0000000..c3c56e9 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperatorTest.java @@ -0,0 +1,132 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.BaseEvaluationTest; +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.parser.ParseException; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class InfixEqualsOperatorTest extends BaseEvaluationTest { + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "1=1 : true", + "1==1 : true", + "0=0 : true", + "1=0 : false", + "0=1 : false", + "21.678=21.678 : true", + "\"abc\"=\"abc\" : true", + "\"abc\"=\"xyz\" : false", + "1+2=4-1 : true", + "-5.2=-5.2 :true" + }) + void testInfixEqualsLiterals(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testInfixEqualsVariables() throws EvaluationException, ParseException { + Expression expression = new Expression("a=b"); + + assertThat( + expression + .with("a", new BigDecimal("1.4")) + .and("b", new BigDecimal("1.4")) + .evaluate() + .getBooleanValue()) + .isTrue(); + + assertThat(expression.with("a", "Hello").and("b", "Hello").evaluate().getBooleanValue()) + .isTrue(); + + assertThat(expression.with("a", "Hello").and("b", "Goodbye").evaluate().getBooleanValue()) + .isFalse(); + + assertThat(expression.with("a", true).and("b", true).evaluate().getBooleanValue()).isTrue(); + + assertThat(expression.with("a", false).and("b", true).evaluate().getBooleanValue()).isFalse(); + } + + @Test + void testInfixEqualsArrays() throws EvaluationException, ParseException { + Expression expression = new Expression("a=b"); + + assertThat( + expression + .with("a", Arrays.asList("a", "b", "c")) + .and("b", Arrays.asList("a", "b", "c")) + .evaluate() + .getBooleanValue()) + .isTrue(); + + assertThat( + expression + .with("a", Arrays.asList("a", "b", "c")) + .and("b", Arrays.asList("c", "b", "a")) + .evaluate() + .getBooleanValue()) + .isFalse(); + } + + @Test + void testInfixEqualsStructures() throws EvaluationException, ParseException { + Expression expression = new Expression("a=b"); + + Map structure1 = + new HashMap<>() { + { + put("a", new BigDecimal(35)); + put("b", new BigDecimal(99)); + } + }; + + Map structure2 = + new HashMap<>() { + { + put("a", new BigDecimal(35)); + put("b", new BigDecimal(99)); + } + }; + + Map structure3 = + new HashMap<>() { + { + put("a", new BigDecimal(45)); + put("b", new BigDecimal(99)); + } + }; + + assertThat(expression.with("a", structure1).and("b", structure2).evaluate().getBooleanValue()) + .isTrue(); + + assertThat(expression.with("a", structure1).and("b", structure3).evaluate().getBooleanValue()) + .isFalse(); + } +} diff --git a/src/test/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperatorTest.java b/src/test/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperatorTest.java new file mode 100644 index 0000000..2c708a9 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperatorTest.java @@ -0,0 +1,49 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import com.ezylang.evalex.BaseEvaluationTest; +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.parser.ParseException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class InfixGreaterEqualsOperatorTest extends BaseEvaluationTest { + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "1>=1 : true", + "2>=1 : true", + "2.1>=2.0 : true", + "2.0>=2.0 : true", + "21.677>=21.678 : false", + "21.679>=21.678 : true", + "\"abc\">=\"abc\" : true", + "\"abd\">=\"abc\" : true", + "\"abc\">=\"xyz\" : false", + "\"ABC\">=\"abc\" : false", + "\"9\">=\"5\" : true", + "\"9\">=\"9\" : true", + "-4>=-4 :true", + "-4>=-5 :true" + }) + void testInfixGreaterEqualsLiterals(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } +} diff --git a/src/test/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperatorTest.java b/src/test/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperatorTest.java new file mode 100644 index 0000000..5f40249 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperatorTest.java @@ -0,0 +1,45 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import com.ezylang.evalex.BaseEvaluationTest; +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.parser.ParseException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class InfixGreaterOperatorTest extends BaseEvaluationTest { + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "1>1 : false", + "2>1 : true", + "2.1>2.0 : true", + "2.0>2.0 : false", + "21.679>21.678 : true", + "\"abd\">\"abc\" : true", + "\"abc\">\"xyz\" : false", + "\"ABC\">\"abc\" : false", + "\"9\">\"5\" : true", + "-4>-5 :true" + }) + void testInfixGreaterLiterals(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } +} diff --git a/src/test/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperatorTest.java b/src/test/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperatorTest.java new file mode 100644 index 0000000..9d4e200 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperatorTest.java @@ -0,0 +1,49 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import com.ezylang.evalex.BaseEvaluationTest; +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.parser.ParseException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class InfixLessEqualsOperatorTest extends BaseEvaluationTest { + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "1<=1 : true", + "1<=2 : true", + "2.0<=2.1 : true", + "2.0<=2.0 : true", + "21.678<=21.677 : false", + "21.678<=21.679 : true", + "\"abc\"<=\"abc\" : true", + "\"abc\"<=\"abd\" : true", + "\"xyz\"<=\"abc\" : false", + "\"abc\"<=\"ABC\" : false", + "\"5\"<=\"9\" : true", + "\"9\"<=\"9\" : true", + "-4<=-4 :true", + "-5<=-4 :true" + }) + void testInfixLessEqualsLiterals(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } +} diff --git a/src/test/java/com/ezylang/evalex/operators/booleans/InfixLessOperatorTest.java b/src/test/java/com/ezylang/evalex/operators/booleans/InfixLessOperatorTest.java new file mode 100644 index 0000000..4a1ca57 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixLessOperatorTest.java @@ -0,0 +1,47 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import com.ezylang.evalex.BaseEvaluationTest; +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.parser.ParseException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class InfixLessOperatorTest extends BaseEvaluationTest { + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "1<1 : false", + "2<1 : false", + "1<2 : true", + "2.1<2.0 : false", + "2.0<2.1 : true", + "2.0<2.0 : false", + "21.678<21.679 : true", + "\"abc\"<\"abd\" : true", + "\"abc\"<\"xyz\" : true", + "\"abc\"<\"ABC\" : false", + "\"5\"<\"9\" : true", + "-5<-4 :true" + }) + void testInfixLessLiterals(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } +} diff --git a/src/test/java/com/ezylang/evalex/operators/booleans/InfixNotEqualsOperatorTest.java b/src/test/java/com/ezylang/evalex/operators/booleans/InfixNotEqualsOperatorTest.java new file mode 100644 index 0000000..848a32a --- /dev/null +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixNotEqualsOperatorTest.java @@ -0,0 +1,132 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.BaseEvaluationTest; +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.parser.ParseException; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class InfixNotEqualsOperatorTest extends BaseEvaluationTest { + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "1!=1 : false", + "1<>1 : false", + "0!=0 : false", + "1!=0 : true", + "0!=1 : true", + "21.678!=21.678 : false", + "\"abc\"!=\"abc\" : false", + "\"abc\"!=\"xyz\" : true", + "1+2!=4-1 : false", + "-5.2!=-5.2 :false" + }) + void testInfixNotEqualsLiterals(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @Test + void testInfixNotEqualsVariables() throws EvaluationException, ParseException { + Expression expression = new Expression("a!=b"); + + assertThat( + expression + .with("a", new BigDecimal("1.4")) + .and("b", new BigDecimal("1.4")) + .evaluate() + .getBooleanValue()) + .isFalse(); + + assertThat(expression.with("a", "Hello").and("b", "Hello").evaluate().getBooleanValue()) + .isFalse(); + + assertThat(expression.with("a", "Hello").and("b", "Goodbye").evaluate().getBooleanValue()) + .isTrue(); + + assertThat(expression.with("a", true).and("b", true).evaluate().getBooleanValue()).isFalse(); + + assertThat(expression.with("a", false).and("b", true).evaluate().getBooleanValue()).isTrue(); + } + + @Test + void testInfixNotEqualsArrays() throws EvaluationException, ParseException { + Expression expression = new Expression("a!=b"); + + assertThat( + expression + .with("a", Arrays.asList("a", "b", "c")) + .and("b", Arrays.asList("a", "b", "c")) + .evaluate() + .getBooleanValue()) + .isFalse(); + + assertThat( + expression + .with("a", Arrays.asList("a", "b", "c")) + .and("b", Arrays.asList("c", "b", "a")) + .evaluate() + .getBooleanValue()) + .isTrue(); + } + + @Test + void testInfixNotEqualsStructures() throws EvaluationException, ParseException { + Expression expression = new Expression("a!=b"); + + Map structure1 = + new HashMap<>() { + { + put("a", new BigDecimal(35)); + put("b", new BigDecimal(99)); + } + }; + + Map structure2 = + new HashMap<>() { + { + put("a", new BigDecimal(35)); + put("b", new BigDecimal(99)); + } + }; + + Map structure3 = + new HashMap<>() { + { + put("a", new BigDecimal(45)); + put("b", new BigDecimal(99)); + } + }; + + assertThat(expression.with("a", structure1).and("b", structure2).evaluate().getBooleanValue()) + .isFalse(); + + assertThat(expression.with("a", structure1).and("b", structure3).evaluate().getBooleanValue()) + .isTrue(); + } +} diff --git a/src/test/java/com/ezylang/evalex/operators/booleans/InfixOrOperatorTest.java b/src/test/java/com/ezylang/evalex/operators/booleans/InfixOrOperatorTest.java new file mode 100644 index 0000000..365df6e --- /dev/null +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixOrOperatorTest.java @@ -0,0 +1,45 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import com.ezylang.evalex.BaseEvaluationTest; +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.parser.ParseException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class InfixOrOperatorTest extends BaseEvaluationTest { + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "1||1 : true", + "1||2 : true", + "0||1 : true", + "0||0 : false", + "22||33 : true", + "\"true\"||\"true\" : true", + "\"true\"||\"false\" : true", + "\"false\"||\"false\" : false", + "(1==1)||(2==3) : true", + "(2>4)||(4<6) :true" + }) + void testInfixLessLiterals(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } +} diff --git a/src/test/java/com/ezylang/evalex/operators/booleans/PrefixNotOperatorTest.java b/src/test/java/com/ezylang/evalex/operators/booleans/PrefixNotOperatorTest.java new file mode 100644 index 0000000..f14cca2 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/operators/booleans/PrefixNotOperatorTest.java @@ -0,0 +1,43 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.booleans; + +import com.ezylang.evalex.BaseEvaluationTest; +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.parser.ParseException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class PrefixNotOperatorTest extends BaseEvaluationTest { + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "!0 : true", + "!1 : false", + "!2.6 : false", + "!-2 : false", + "!\"true\" : false", + "!\"false\" : true", + "!(1==1) : false", + "!(2==3) : true" + }) + void testInfixLessLiterals(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/ASTNodeTest.java b/src/test/java/com/ezylang/evalex/parser/ASTNodeTest.java new file mode 100644 index 0000000..818391c --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/ASTNodeTest.java @@ -0,0 +1,67 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.functions.basic.MinFunction; +import com.ezylang.evalex.operators.arithmetic.InfixPlusOperator; +import com.ezylang.evalex.operators.arithmetic.PrefixMinusOperator; +import com.ezylang.evalex.parser.Token.TokenType; +import org.junit.jupiter.api.Test; + +class ASTNodeTest { + final Token variable = new Token(1, "variable", TokenType.VARIABLE_OR_CONSTANT); + + @Test + void testJSONSingle() { + ASTNode node = new ASTNode(variable); + + assertThat(node.toJSON()) + .isEqualTo("{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"variable\"}"); + } + + @Test + void testJSONPrefix() { + Token token = new Token(1, "-", TokenType.PREFIX_OPERATOR, new PrefixMinusOperator()); + ASTNode node = new ASTNode(token, new ASTNode(variable)); + + assertThat(node.toJSON()) + .isEqualTo( + "{\"type\":\"PREFIX_OPERATOR\",\"value\":\"-\",\"children\":[{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"variable\"}]}"); + } + + @Test + void testJSONInfix() { + Token token = new Token(1, "+", TokenType.INFIX_OPERATOR, new InfixPlusOperator()); + ASTNode node = new ASTNode(token, new ASTNode(variable), new ASTNode(variable)); + + assertThat(node.toJSON()) + .isEqualTo( + "{\"type\":\"INFIX_OPERATOR\",\"value\":\"+\",\"children\":[{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"variable\"},{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"variable\"}]}"); + } + + @Test + void testJSONFunction() { + Token token = new Token(1, "+", TokenType.FUNCTION, new MinFunction()); + ASTNode node = + new ASTNode(token, new ASTNode(variable), new ASTNode(variable), new ASTNode(variable)); + + assertThat(node.toJSON()) + .isEqualTo( + "{\"type\":\"FUNCTION\",\"value\":\"+\",\"children\":[{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"variable\"},{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"variable\"},{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"variable\"}]}"); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/BaseParserTest.java b/src/test/java/com/ezylang/evalex/parser/BaseParserTest.java new file mode 100644 index 0000000..5eb8784 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/BaseParserTest.java @@ -0,0 +1,48 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.config.TestConfigurationProvider; +import java.util.List; + +/** */ +public abstract class BaseParserTest { + + ExpressionConfiguration configuration = + TestConfigurationProvider.StandardConfigurationWithAdditionalTestOperators; + + void assertAllTokensParsedCorrectly(String input, Token... expectedTokens) throws ParseException { + List tokensParsed = new Tokenizer(input, configuration).parse(); + + assertThat(tokensParsed).containsExactly(expectedTokens); + } + + /** + * Compares if the generated abstract syntax tree is correct. To visualize the generated JSON in a + * tree format, i.e. to display the AST, you can use the following online service:
+ * Online JSON to Tree Diagram Converter + */ + void assertASTTreeIsEqualTo(String expression, String treeJSON) throws ParseException { + + List tokensParsed = new Tokenizer(expression, configuration).parse(); + ASTNode root = + new ShuntingYardConverter(expression, tokensParsed, configuration).toAbstractSyntaxTree(); + assertThat(root.toJSON()).isEqualTo(treeJSON); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/ParseExceptionTest.java b/src/test/java/com/ezylang/evalex/parser/ParseExceptionTest.java new file mode 100644 index 0000000..a36b665 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/ParseExceptionTest.java @@ -0,0 +1,33 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class ParseExceptionTest { + + @Test + void testToString() { + ParseException exception = new ParseException(2, 4, "test", "message"); + + assertThat(exception) + .hasToString( + "ParseException(super=BaseException(startPosition=2, endPosition=4, tokenString=test," + + " message=message))"); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/ShuntingYardArrayTest.java b/src/test/java/com/ezylang/evalex/parser/ShuntingYardArrayTest.java new file mode 100644 index 0000000..b396999 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/ShuntingYardArrayTest.java @@ -0,0 +1,49 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import org.junit.jupiter.api.Test; + +class ShuntingYardArrayTest extends BaseParserTest { + + @Test + void testSimpleArray() throws ParseException { + assertASTTreeIsEqualTo( + "a[0]", + "{\"type\":\"ARRAY_INDEX\",\"value\":\"[\",\"children\":[{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"a\"},{\"type\":\"NUMBER_LITERAL\",\"value\":\"0\"}]}"); + } + + @Test + void testArrayExpression() throws ParseException { + assertASTTreeIsEqualTo( + "a[x+y]", + "{\"type\":\"ARRAY_INDEX\",\"value\":\"[\",\"children\":[{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"a\"},{\"type\":\"INFIX_OPERATOR\",\"value\":\"+\",\"children\":[{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"x\"},{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"y\"}]}]}"); + } + + @Test + void testArrayNested() throws ParseException { + assertASTTreeIsEqualTo( + "a[b[1]]", + "{\"type\":\"ARRAY_INDEX\",\"value\":\"[\",\"children\":[{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"a\"},{\"type\":\"ARRAY_INDEX\",\"value\":\"[\",\"children\":[{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"b\"},{\"type\":\"NUMBER_LITERAL\",\"value\":\"1\"}]}]}"); + } + + @Test + void testComplex() throws ParseException { + assertASTTreeIsEqualTo( + "a[b[100*(a+b)]-c[2+d[x+y]]]", + "{\"type\":\"ARRAY_INDEX\",\"value\":\"[\",\"children\":[{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"a\"},{\"type\":\"INFIX_OPERATOR\",\"value\":\"-\",\"children\":[{\"type\":\"ARRAY_INDEX\",\"value\":\"[\",\"children\":[{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"b\"},{\"type\":\"INFIX_OPERATOR\",\"value\":\"*\",\"children\":[{\"type\":\"NUMBER_LITERAL\",\"value\":\"100\"},{\"type\":\"INFIX_OPERATOR\",\"value\":\"+\",\"children\":[{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"a\"},{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"b\"}]}]}]},{\"type\":\"ARRAY_INDEX\",\"value\":\"[\",\"children\":[{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"c\"},{\"type\":\"INFIX_OPERATOR\",\"value\":\"+\",\"children\":[{\"type\":\"NUMBER_LITERAL\",\"value\":\"2\"},{\"type\":\"ARRAY_INDEX\",\"value\":\"[\",\"children\":[{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"d\"},{\"type\":\"INFIX_OPERATOR\",\"value\":\"+\",\"children\":[{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"x\"},{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"y\"}]}]}]}]}]}]}"); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/ShuntingYardConverterTest.java b/src/test/java/com/ezylang/evalex/parser/ShuntingYardConverterTest.java new file mode 100644 index 0000000..de16046 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/ShuntingYardConverterTest.java @@ -0,0 +1,80 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import org.junit.jupiter.api.Test; + +class ShuntingYardConverterTest extends BaseParserTest { + + @Test + void testSingleNumber() throws ParseException { + assertASTTreeIsEqualTo("1", "{\"type\":\"NUMBER_LITERAL\",\"value\":\"1\"}"); + } + + @Test + void testSingleVariable() throws ParseException { + assertASTTreeIsEqualTo("a", "{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"a\"}"); + } + + @Test + void testPrefix() throws ParseException { + assertASTTreeIsEqualTo( + "-1", + "{\"type\":\"PREFIX_OPERATOR\",\"value\":\"-\",\"children\":[{\"type\":\"NUMBER_LITERAL\",\"value\":\"1\"}]}"); + } + + @Test + void testPostfix() throws ParseException { + assertASTTreeIsEqualTo( + "1?", + "{\"type\":\"POSTFIX_OPERATOR\",\"value\":\"?\",\"children\":[{\"type\":\"NUMBER_LITERAL\",\"value\":\"1\"}]}"); + } + + @Test + void testPrefixPostfix() throws ParseException { + assertASTTreeIsEqualTo( + "-1?", + "{\"type\":\"PREFIX_OPERATOR\",\"value\":\"-\",\"children\":[{\"type\":\"POSTFIX_OPERATOR\",\"value\":\"?\",\"children\":[{\"type\":\"NUMBER_LITERAL\",\"value\":\"1\"}]}]}"); + } + + @Test + void testSequential() throws ParseException { + assertASTTreeIsEqualTo( + "1+2+3-3-2-1", + "{\"type\":\"INFIX_OPERATOR\",\"value\":\"-\",\"children\":[{\"type\":\"INFIX_OPERATOR\",\"value\":\"-\",\"children\":[{\"type\":\"INFIX_OPERATOR\",\"value\":\"-\",\"children\":[{\"type\":\"INFIX_OPERATOR\",\"value\":\"+\",\"children\":[{\"type\":\"INFIX_OPERATOR\",\"value\":\"+\",\"children\":[{\"type\":\"NUMBER_LITERAL\",\"value\":\"1\"},{\"type\":\"NUMBER_LITERAL\",\"value\":\"2\"}]},{\"type\":\"NUMBER_LITERAL\",\"value\":\"3\"}]},{\"type\":\"NUMBER_LITERAL\",\"value\":\"3\"}]},{\"type\":\"NUMBER_LITERAL\",\"value\":\"2\"}]},{\"type\":\"NUMBER_LITERAL\",\"value\":\"1\"}]}"); + } + + @Test + void testPrecedence() throws ParseException { + assertASTTreeIsEqualTo( + "1+2*3-3^2-1/4", + "{\"type\":\"INFIX_OPERATOR\",\"value\":\"-\",\"children\":[{\"type\":\"INFIX_OPERATOR\",\"value\":\"-\",\"children\":[{\"type\":\"INFIX_OPERATOR\",\"value\":\"+\",\"children\":[{\"type\":\"NUMBER_LITERAL\",\"value\":\"1\"},{\"type\":\"INFIX_OPERATOR\",\"value\":\"*\",\"children\":[{\"type\":\"NUMBER_LITERAL\",\"value\":\"2\"},{\"type\":\"NUMBER_LITERAL\",\"value\":\"3\"}]}]},{\"type\":\"INFIX_OPERATOR\",\"value\":\"^\",\"children\":[{\"type\":\"NUMBER_LITERAL\",\"value\":\"3\"},{\"type\":\"NUMBER_LITERAL\",\"value\":\"2\"}]}]},{\"type\":\"INFIX_OPERATOR\",\"value\":\"/\",\"children\":[{\"type\":\"NUMBER_LITERAL\",\"value\":\"1\"},{\"type\":\"NUMBER_LITERAL\",\"value\":\"4\"}]}]}"); + } + + @Test + void testBraces() throws ParseException { + assertASTTreeIsEqualTo( + "2*(1/(2+3))", + "{\"type\":\"INFIX_OPERATOR\",\"value\":\"*\",\"children\":[{\"type\":\"NUMBER_LITERAL\",\"value\":\"2\"},{\"type\":\"INFIX_OPERATOR\",\"value\":\"/\",\"children\":[{\"type\":\"NUMBER_LITERAL\",\"value\":\"1\"},{\"type\":\"INFIX_OPERATOR\",\"value\":\"+\",\"children\":[{\"type\":\"NUMBER_LITERAL\",\"value\":\"2\"},{\"type\":\"NUMBER_LITERAL\",\"value\":\"3\"}]}]}]}"); + } + + @Test + void testFunctions() throws ParseException { + assertASTTreeIsEqualTo( + "MAX(1,2,3)-MIN(3,2,SUM(1,2,3))", + "{\"type\":\"INFIX_OPERATOR\",\"value\":\"-\",\"children\":[{\"type\":\"FUNCTION\",\"value\":\"MAX\",\"children\":[{\"type\":\"NUMBER_LITERAL\",\"value\":\"1\"},{\"type\":\"NUMBER_LITERAL\",\"value\":\"2\"},{\"type\":\"NUMBER_LITERAL\",\"value\":\"3\"}]},{\"type\":\"FUNCTION\",\"value\":\"MIN\",\"children\":[{\"type\":\"NUMBER_LITERAL\",\"value\":\"3\"},{\"type\":\"NUMBER_LITERAL\",\"value\":\"2\"},{\"type\":\"FUNCTION\",\"value\":\"SUM\",\"children\":[{\"type\":\"NUMBER_LITERAL\",\"value\":\"1\"},{\"type\":\"NUMBER_LITERAL\",\"value\":\"2\"},{\"type\":\"NUMBER_LITERAL\",\"value\":\"3\"}]}]}]}"); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/ShuntingYardExceptionsTest.java b/src/test/java/com/ezylang/evalex/parser/ShuntingYardExceptionsTest.java new file mode 100644 index 0000000..342168f --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/ShuntingYardExceptionsTest.java @@ -0,0 +1,136 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import static com.ezylang.evalex.parser.Token.TokenType.FUNCTION_PARAM_START; +import static com.ezylang.evalex.parser.Token.TokenType.INFIX_OPERATOR; +import static com.ezylang.evalex.parser.Token.TokenType.POSTFIX_OPERATOR; +import static com.ezylang.evalex.parser.Token.TokenType.PREFIX_OPERATOR; +import static com.ezylang.evalex.parser.Token.TokenType.STRUCTURE_SEPARATOR; +import static com.ezylang.evalex.parser.Token.TokenType.VARIABLE_OR_CONSTANT; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.operators.arithmetic.InfixMultiplicationOperator; +import com.ezylang.evalex.operators.arithmetic.PrefixMinusOperator; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ShuntingYardExceptionsTest extends BaseParserTest { + + @Test + void testUnexpectedToken() { + List tokens = List.of(new Token(1, "x", FUNCTION_PARAM_START)); + assertThatThrownBy(new ShuntingYardConverter("x", tokens, configuration)::toAbstractSyntaxTree) + .isInstanceOf(ParseException.class) + .hasMessage("Unexpected token of type 'FUNCTION_PARAM_START'"); + } + + @Test + void testMissingPrefixOperand() { + List tokens = List.of(new Token(1, "-", PREFIX_OPERATOR, new PrefixMinusOperator())); + assertThatThrownBy(new ShuntingYardConverter("-", tokens, configuration)::toAbstractSyntaxTree) + .isInstanceOf(ParseException.class) + .hasMessage("Missing operand for operator"); + } + + @Test + void testMissingSecondInfixOperand() { + List tokens = + Arrays.asList( + new Token(1, "2", VARIABLE_OR_CONSTANT), + new Token(2, "*", INFIX_OPERATOR, new InfixMultiplicationOperator())); + assertThatThrownBy(new ShuntingYardConverter("2*", tokens, configuration)::toAbstractSyntaxTree) + .isInstanceOf(ParseException.class) + .hasMessage("Missing second operand for operator"); + } + + @Test + void testEmptyExpression() { + Expression expression = new Expression(""); + + assertThatThrownBy(expression::evaluate) + .isInstanceOf(ParseException.class) + .hasMessage("Empty expression"); + } + + @Test + void testEmptyExpressionBraces() { + Expression expression = new Expression("()"); + + assertThatThrownBy(expression::evaluate) + .isInstanceOf(ParseException.class) + .hasMessage("Empty expression"); + } + + @Test + void testDoubleStructureOperator() { + List tokens = + List.of(new Token(1, ".", STRUCTURE_SEPARATOR), new Token(2, ".", STRUCTURE_SEPARATOR)); + assertThatThrownBy(new ShuntingYardConverter("..", tokens, configuration)::toAbstractSyntaxTree) + .isInstanceOf(ParseException.class) + .hasMessage("Missing operand for operator"); + } + + @Test + void testStructureFollowsPostfixOperator() { + List tokens = + List.of(new Token(1, ".", STRUCTURE_SEPARATOR), new Token(2, "!", POSTFIX_OPERATOR)); + assertThatThrownBy(new ShuntingYardConverter("..", tokens, configuration)::toAbstractSyntaxTree) + .isInstanceOf(ParseException.class) + .hasMessage("Missing operand for operator"); + } + + @Test + void testStructureFollowsTwoPostfixOperators() { + List tokens = + List.of( + new Token(1, ".", STRUCTURE_SEPARATOR), + new Token(2, "!", POSTFIX_OPERATOR), + new Token(2, "!", POSTFIX_OPERATOR)); + assertThatThrownBy(new ShuntingYardConverter("..", tokens, configuration)::toAbstractSyntaxTree) + .isInstanceOf(ParseException.class) + .hasMessage("Missing operand for operator"); + } + + @Test + void testFunctionNotEnoughParameters() { + Expression expression = new Expression("ROUND(2)"); + + assertThatThrownBy(expression::evaluate) + .isInstanceOf(ParseException.class) + .hasMessage("Not enough parameters for function"); + } + + @Test + void testFunctionNotEnoughParametersForVarArgs() { + Expression expression = new Expression("MIN()"); + + assertThatThrownBy(expression::evaluate) + .isInstanceOf(ParseException.class) + .hasMessage("Not enough parameters for function"); + } + + @Test + void testFunctionTooManyParameters() { + Expression expression = new Expression("ROUND(1,2,3)"); + + assertThatThrownBy(expression::evaluate) + .isInstanceOf(ParseException.class) + .hasMessage("Too many parameters for function"); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/ShuntingYardStructureTest.java b/src/test/java/com/ezylang/evalex/parser/ShuntingYardStructureTest.java new file mode 100644 index 0000000..fe99683 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/ShuntingYardStructureTest.java @@ -0,0 +1,61 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.Expression; +import org.junit.jupiter.api.Test; + +class ShuntingYardStructureTest extends BaseParserTest { + + @Test + void testSimpleStructure() throws ParseException { + assertASTTreeIsEqualTo( + "a.b", + "{\"type\":\"STRUCTURE_SEPARATOR\",\"value\":\".\",\"children\":[{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"a\"},{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"b\"}]}"); + } + + @Test + void testTripleStructure() throws ParseException { + assertASTTreeIsEqualTo( + "a.b.c", + "{\"type\":\"STRUCTURE_SEPARATOR\",\"value\":\".\",\"children\":[{\"type\":\"STRUCTURE_SEPARATOR\",\"value\":\".\",\"children\":[{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"a\"},{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"b\"}]},{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"c\"}]}"); + } + + @Test + void testArrayCombination() throws ParseException { + assertASTTreeIsEqualTo( + "order[4].position[2].amount", + "{\"type\":\"STRUCTURE_SEPARATOR\",\"value\":\".\",\"children\":[{\"type\":\"ARRAY_INDEX\",\"value\":\"[\",\"children\":[{\"type\":\"STRUCTURE_SEPARATOR\",\"value\":\".\",\"children\":[{\"type\":\"ARRAY_INDEX\",\"value\":\"[\",\"children\":[{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"order\"},{\"type\":\"NUMBER_LITERAL\",\"value\":\"4\"}]},{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"position\"}]},{\"type\":\"NUMBER_LITERAL\",\"value\":\"2\"}]},{\"type\":\"VARIABLE_OR_CONSTANT\",\"value\":\"amount\"}]}"); + } + + @Test + void testExceptionStructureEnd() { + Expression expression = new Expression("a."); + assertThatThrownBy(expression::getAbstractSyntaxTree) + .isInstanceOf(ParseException.class) + .hasMessage("Missing second operand for operator"); + } + + @Test + void testExceptionStructureStart() { + Expression expression = new Expression(".a"); + assertThatThrownBy(expression::getAbstractSyntaxTree) + .isInstanceOf(ParseException.class) + .hasMessage("Structure separator not allowed here"); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/TokenTest.java b/src/test/java/com/ezylang/evalex/parser/TokenTest.java new file mode 100644 index 0000000..c4e19e5 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/TokenTest.java @@ -0,0 +1,77 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.parser.Token.TokenType; +import org.junit.jupiter.api.Test; + +class TokenTest { + + final ExpressionConfiguration expressionConfiguration = + ExpressionConfiguration.defaultConfiguration(); + + @Test + void testTokenCreation() { + int counter = 0; + for (TokenType type : TokenType.values()) { + counter++; + String tokenString = + type == TokenType.NUMBER_LITERAL ? Integer.toString(counter) : "token" + counter; + Token token = new Token(counter, tokenString, type); + + assertThat(token.getType()).isEqualTo(type); + assertThat(token.getStartPosition()).isEqualTo(counter); + assertThat(token.getValue()).isEqualTo(tokenString); + assertThat(token.getFunctionDefinition()).isNull(); + assertThat(token.getOperatorDefinition()).isNull(); + } + } + + @Test + void testFunctionToken() { + Token token = + new Token( + 3, + "MAX", + TokenType.FUNCTION, + expressionConfiguration.getFunctionDictionary().getFunction("MAX")); + + assertThat(token.getStartPosition()).isEqualTo(3); + assertThat(token.getValue()).isEqualTo("MAX"); + assertThat(token.getType()).isEqualTo(TokenType.FUNCTION); + assertThat(token.getFunctionDefinition()).isNotNull(); + assertThat(token.getOperatorDefinition()).isNull(); + } + + @Test + void testOperatorToken() { + Token token = + new Token( + 1, + "+", + TokenType.INFIX_OPERATOR, + expressionConfiguration.getOperatorDictionary().getInfixOperator("+")); + + assertThat(token.getStartPosition()).isEqualTo(1); + assertThat(token.getValue()).isEqualTo("+"); + assertThat(token.getType()).isEqualTo(TokenType.INFIX_OPERATOR); + assertThat(token.getFunctionDefinition()).isNull(); + assertThat(token.getOperatorDefinition()).isNotNull(); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/TokenizerArrayTest.java b/src/test/java/com/ezylang/evalex/parser/TokenizerArrayTest.java new file mode 100644 index 0000000..c917872 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/TokenizerArrayTest.java @@ -0,0 +1,111 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.parser.Token.TokenType; +import org.junit.jupiter.api.Test; + +class TokenizerArrayTest extends BaseParserTest { + + @Test + void testArraySimple() throws ParseException { + assertAllTokensParsedCorrectly( + "a[1]", + new Token(1, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(2, "[", TokenType.ARRAY_OPEN), + new Token(3, "1", TokenType.NUMBER_LITERAL), + new Token(4, "]", TokenType.ARRAY_CLOSE)); + } + + @Test + void testArrayNested() throws ParseException { + assertAllTokensParsedCorrectly( + "a[b[2] + c[3]]", + new Token(1, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(2, "[", TokenType.ARRAY_OPEN), + new Token(3, "b", TokenType.VARIABLE_OR_CONSTANT), + new Token(4, "[", TokenType.ARRAY_OPEN), + new Token(5, "2", TokenType.NUMBER_LITERAL), + new Token(6, "]", TokenType.ARRAY_CLOSE), + new Token(8, "+", TokenType.INFIX_OPERATOR), + new Token(10, "c", TokenType.VARIABLE_OR_CONSTANT), + new Token(11, "[", TokenType.ARRAY_OPEN), + new Token(12, "3", TokenType.NUMBER_LITERAL), + new Token(13, "]", TokenType.ARRAY_CLOSE), + new Token(14, "]", TokenType.ARRAY_CLOSE)); + } + + @Test + void testMissingClosingArray() { + assertThatThrownBy(() -> new Tokenizer("a[2+4", configuration).parse()) + .isEqualTo(new ParseException(1, 5, "a[2+4", "Closing array not found")); + } + + @Test + void testUnexpectedClosingArray() { + assertThatThrownBy(() -> new Tokenizer("a[2+4]]", configuration).parse()) + .isEqualTo(new ParseException(7, 7, "]", "Unexpected closing array")); + } + + @Test + void testOpenArrayNotAllowedBeginning() { + assertThatThrownBy(() -> new Tokenizer("[1]", configuration).parse()) + .isEqualTo(new ParseException(1, 1, "[", "Array open not allowed here")); + } + + @Test + void testOpenArrayNotAllowedAfterOperator() { + assertThatThrownBy(() -> new Tokenizer("1+[1]", configuration).parse()) + .isEqualTo(new ParseException(3, 3, "[", "Array open not allowed here")); + } + + @Test + void testOpenArrayNotAllowedAfterBrace() { + assertThatThrownBy(() -> new Tokenizer("([1]", configuration).parse()) + .isEqualTo(new ParseException(2, 2, "[", "Array open not allowed here")); + } + + @Test + void testCloseArrayNotAllowedBeginning() { + assertThatThrownBy(() -> new Tokenizer("]", configuration).parse()) + .isEqualTo(new ParseException(1, 1, "]", "Array close not allowed here")); + } + + @Test + void testCloseArrayNotAllowedAfterBrace() { + assertThatThrownBy(() -> new Tokenizer("(]", configuration).parse()) + .isEqualTo(new ParseException(2, 2, "]", "Array close not allowed here")); + } + + @Test + void testArraysNotAllowedOpen() { + ExpressionConfiguration config = ExpressionConfiguration.builder().arraysAllowed(false).build(); + + assertThatThrownBy(() -> new Tokenizer("a[0]", config).parse()) + .isEqualTo(new ParseException(2, 2, "[", "Undefined operator '['")); + } + + @Test + void testArraysNotAllowedClose() { + ExpressionConfiguration config = ExpressionConfiguration.builder().arraysAllowed(false).build(); + + assertThatThrownBy(() -> new Tokenizer("]", config).parse()) + .isEqualTo(new ParseException(1, 1, "]", "Undefined operator ']'")); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/TokenizerBracesTest.java b/src/test/java/com/ezylang/evalex/parser/TokenizerBracesTest.java new file mode 100644 index 0000000..c0d23f6 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/TokenizerBracesTest.java @@ -0,0 +1,63 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.parser.Token.TokenType; +import org.junit.jupiter.api.Test; + +class TokenizerBracesTest extends BaseParserTest { + + @Test + void testBracesSimple() throws ParseException { + assertAllTokensParsedCorrectly( + "(a + b)", + new Token(1, "(", TokenType.BRACE_OPEN), + new Token(2, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(4, "+", TokenType.INFIX_OPERATOR), + new Token(6, "b", TokenType.VARIABLE_OR_CONSTANT), + new Token(7, ")", TokenType.BRACE_CLOSE)); + } + + @Test + void testBracesNested() throws ParseException { + + assertAllTokensParsedCorrectly( + "(a+(b+c))", + new Token(1, "(", TokenType.BRACE_OPEN), + new Token(2, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(3, "+", TokenType.INFIX_OPERATOR), + new Token(4, "(", TokenType.BRACE_OPEN), + new Token(5, "b", TokenType.VARIABLE_OR_CONSTANT), + new Token(6, "+", TokenType.INFIX_OPERATOR), + new Token(7, "c", TokenType.VARIABLE_OR_CONSTANT), + new Token(8, ")", TokenType.BRACE_CLOSE), + new Token(9, ")", TokenType.BRACE_CLOSE)); + } + + @Test + void testMissingClosingBrace() { + assertThatThrownBy(() -> new Tokenizer("(2+4", configuration).parse()) + .isEqualTo(new ParseException(1, 4, "(2+4", "Closing brace not found")); + } + + @Test + void testUnexpectedClosingBrace() { + assertThatThrownBy(() -> new Tokenizer("(2+4))", configuration).parse()) + .isEqualTo(new ParseException(6, 6, ")", "Unexpected closing brace")); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/TokenizerExpressionTest.java b/src/test/java/com/ezylang/evalex/parser/TokenizerExpressionTest.java new file mode 100644 index 0000000..37c61e2 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/TokenizerExpressionTest.java @@ -0,0 +1,60 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.parser.Token.TokenType; +import org.junit.jupiter.api.Test; + +class TokenizerExpressionTest extends BaseParserTest { + + @Test + void testSingleNumber() throws ParseException { + assertAllTokensParsedCorrectly("1", new Token(1, "1", TokenType.NUMBER_LITERAL)); + } + + @Test + void testSingleVariable() throws ParseException { + assertAllTokensParsedCorrectly("a", new Token(1, "a", TokenType.VARIABLE_OR_CONSTANT)); + } + + @Test + void testSimple() throws ParseException { + assertAllTokensParsedCorrectly( + "a+123", + new Token(1, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(2, "+", TokenType.INFIX_OPERATOR), + new Token(3, "123", TokenType.NUMBER_LITERAL)); + } + + @Test + void testTwo() throws ParseException { + assertAllTokensParsedCorrectly( + "a+123+c", + new Token(1, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(2, "+", TokenType.INFIX_OPERATOR), + new Token(3, "123", TokenType.NUMBER_LITERAL), + new Token(6, "+", TokenType.INFIX_OPERATOR), + new Token(7, "c", TokenType.VARIABLE_OR_CONSTANT)); + } + + @Test + void testUndefinedOperator() { + assertThatThrownBy(() -> new Tokenizer("a $ b", configuration).parse()) + .isEqualTo(new ParseException(3, 3, "$", "Undefined operator '$'")); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/TokenizerFunctionsTest.java b/src/test/java/com/ezylang/evalex/parser/TokenizerFunctionsTest.java new file mode 100644 index 0000000..14b2c6a --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/TokenizerFunctionsTest.java @@ -0,0 +1,142 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.config.TestConfigurationProvider.DummyFunction; +import com.ezylang.evalex.parser.Token.TokenType; +import org.junit.jupiter.api.Test; + +class TokenizerFunctionsTest extends BaseParserTest { + + @Test + void testSimple() throws ParseException { + configuration.getFunctionDictionary().addFunction("f", new DummyFunction()); + assertAllTokensParsedCorrectly( + "f(x)", + new Token(1, "f", TokenType.FUNCTION), + new Token(2, "(", TokenType.BRACE_OPEN), + new Token(3, "x", TokenType.VARIABLE_OR_CONSTANT), + new Token(4, ")", TokenType.BRACE_CLOSE)); + } + + @Test + void testBlanks() throws ParseException { + configuration.getFunctionDictionary().addFunction("f", new DummyFunction()); + assertAllTokensParsedCorrectly( + "f (x)", + new Token(1, "f", TokenType.FUNCTION), + new Token(3, "(", TokenType.BRACE_OPEN), + new Token(4, "x", TokenType.VARIABLE_OR_CONSTANT), + new Token(5, ")", TokenType.BRACE_CLOSE)); + } + + @Test + void testUnderscores() throws ParseException { + configuration.getFunctionDictionary().addFunction("_f_x_", new DummyFunction()); + assertAllTokensParsedCorrectly( + "_f_x_(x)", + new Token(1, "_f_x_", TokenType.FUNCTION), + new Token(6, "(", TokenType.BRACE_OPEN), + new Token(7, "x", TokenType.VARIABLE_OR_CONSTANT), + new Token(8, ")", TokenType.BRACE_CLOSE)); + } + + @Test + void testWithNumbers() throws ParseException { + configuration.getFunctionDictionary().addFunction("f1x2", new DummyFunction()); + assertAllTokensParsedCorrectly( + "f1x2(x)", + new Token(1, "f1x2", TokenType.FUNCTION), + new Token(5, "(", TokenType.BRACE_OPEN), + new Token(6, "x", TokenType.VARIABLE_OR_CONSTANT), + new Token(7, ")", TokenType.BRACE_CLOSE)); + } + + @Test + void testWithMoreParameters() throws ParseException { + assertAllTokensParsedCorrectly( + "SUM(a, 2, 3)", + new Token(1, "SUM", TokenType.FUNCTION), + new Token(4, "(", TokenType.BRACE_OPEN), + new Token(5, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(6, ",", TokenType.COMMA), + new Token(8, "2", TokenType.NUMBER_LITERAL), + new Token(9, ",", TokenType.COMMA), + new Token(11, "3", TokenType.NUMBER_LITERAL), + new Token(12, ")", TokenType.BRACE_CLOSE)); + } + + @Test + void testWithMixedParameters() throws ParseException { + assertAllTokensParsedCorrectly( + "TEST(a, \"hello\", 3)", + new Token(1, "TEST", TokenType.FUNCTION), + new Token(5, "(", TokenType.BRACE_OPEN), + new Token(6, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(7, ",", TokenType.COMMA), + new Token(9, "hello", TokenType.STRING_LITERAL), + new Token(16, ",", TokenType.COMMA), + new Token(18, "3", TokenType.NUMBER_LITERAL), + new Token(19, ")", TokenType.BRACE_CLOSE)); + } + + @Test + void testFunctionInParameter() throws ParseException { + assertAllTokensParsedCorrectly( + "TEST(a, FACT(x), 3)", + new Token(1, "TEST", TokenType.FUNCTION), + new Token(5, "(", TokenType.BRACE_OPEN), + new Token(6, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(7, ",", TokenType.COMMA), + new Token(9, "FACT", TokenType.FUNCTION), + new Token(13, "(", TokenType.BRACE_OPEN), + new Token(14, "x", TokenType.VARIABLE_OR_CONSTANT), + new Token(15, ")", TokenType.BRACE_CLOSE), + new Token(16, ",", TokenType.COMMA), + new Token(18, "3", TokenType.NUMBER_LITERAL), + new Token(19, ")", TokenType.BRACE_CLOSE)); + } + + @Test + void testFunctionInParameterInFunctionParameter() throws ParseException { + assertAllTokensParsedCorrectly( + "SUM(a,FACT(MIN(x,y)),3)", + new Token(1, "SUM", TokenType.FUNCTION), + new Token(4, "(", TokenType.BRACE_OPEN), + new Token(5, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(6, ",", TokenType.COMMA), + new Token(7, "FACT", TokenType.FUNCTION), + new Token(11, "(", TokenType.BRACE_OPEN), + new Token(12, "MIN", TokenType.FUNCTION), + new Token(15, "(", TokenType.BRACE_OPEN), + new Token(16, "x", TokenType.VARIABLE_OR_CONSTANT), + new Token(17, ",", TokenType.COMMA), + new Token(18, "y", TokenType.VARIABLE_OR_CONSTANT), + new Token(19, ")", TokenType.BRACE_CLOSE), + new Token(20, ")", TokenType.BRACE_CLOSE), + new Token(21, ",", TokenType.COMMA), + new Token(22, "3", TokenType.NUMBER_LITERAL), + new Token(23, ")", TokenType.BRACE_CLOSE)); + } + + @Test + void testUndefinedFunction() { + assertThatThrownBy(() -> new Tokenizer("a(b)", configuration).parse()) + .isEqualTo(new ParseException(1, 2, "a", "Undefined function 'a'")); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/TokenizerImplicitMultiplicationTest.java b/src/test/java/com/ezylang/evalex/parser/TokenizerImplicitMultiplicationTest.java new file mode 100644 index 0000000..ddd588f --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/TokenizerImplicitMultiplicationTest.java @@ -0,0 +1,62 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.parser.Token.TokenType; +import org.junit.jupiter.api.Test; + +class TokenizerImplicitMultiplicationTest extends BaseParserTest { + + @Test + void testImplicitBraces() throws ParseException { + assertAllTokensParsedCorrectly( + "(a+b)(a-b)", + new Token(1, "(", TokenType.BRACE_OPEN), + new Token(2, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(3, "+", TokenType.INFIX_OPERATOR), + new Token(4, "b", TokenType.VARIABLE_OR_CONSTANT), + new Token(5, ")", TokenType.BRACE_CLOSE), + new Token(6, "*", TokenType.INFIX_OPERATOR), + new Token(6, "(", TokenType.BRACE_OPEN), + new Token(7, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(8, "-", TokenType.INFIX_OPERATOR), + new Token(9, "b", TokenType.VARIABLE_OR_CONSTANT), + new Token(10, ")", TokenType.BRACE_CLOSE)); + } + + @Test + void testImplicitNumber() throws ParseException { + assertAllTokensParsedCorrectly( + "2(x)", + new Token(1, "2", TokenType.NUMBER_LITERAL), + new Token(2, "*", TokenType.INFIX_OPERATOR), + new Token(2, "(", TokenType.BRACE_OPEN), + new Token(3, "x", TokenType.VARIABLE_OR_CONSTANT), + new Token(4, ")", TokenType.BRACE_CLOSE)); + } + + @Test + void testImplicitMultiplicationNotAllowed() { + ExpressionConfiguration config = + ExpressionConfiguration.builder().implicitMultiplicationAllowed(false).build(); + + assertThatThrownBy(() -> new Tokenizer("2(x+y)", config).parse()) + .isEqualTo(new ParseException(2, 2, "(", "Missing operator")); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/TokenizerLiteralOperatorsTest.java b/src/test/java/com/ezylang/evalex/parser/TokenizerLiteralOperatorsTest.java new file mode 100644 index 0000000..802bfdf --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/TokenizerLiteralOperatorsTest.java @@ -0,0 +1,95 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_AND; +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_OR; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.InfixOperator; +import com.ezylang.evalex.operators.PostfixOperator; +import com.ezylang.evalex.operators.PrefixOperator; +import com.ezylang.evalex.parser.Token.TokenType; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TokenizerLiteralOperatorsTest extends BaseParserTest { + + @BeforeEach + public void setup() { + configuration = + configuration.withAdditionalOperators( + Map.entry("AND", new AndOperator()), + Map.entry("OR", new OrOperator()), + Map.entry("NOT", new NotOperator()), + Map.entry("DENIED", new DeniedOperator())); + } + + @Test + void testAndOrNot() throws ParseException { + assertAllTokensParsedCorrectly( + "NOT a AND b DENIED OR NOT(c)", + new Token(1, "NOT", TokenType.PREFIX_OPERATOR), + new Token(5, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(7, "AND", TokenType.INFIX_OPERATOR), + new Token(11, "b", TokenType.VARIABLE_OR_CONSTANT), + new Token(13, "DENIED", TokenType.POSTFIX_OPERATOR), + new Token(20, "OR", TokenType.INFIX_OPERATOR), + new Token(23, "NOT", TokenType.PREFIX_OPERATOR), + new Token(26, "(", TokenType.BRACE_OPEN), + new Token(27, "c", TokenType.VARIABLE_OR_CONSTANT), + new Token(28, ")", TokenType.BRACE_CLOSE)); + } + + @InfixOperator(precedence = OPERATOR_PRECEDENCE_AND) + static class AndOperator extends AbstractOperator { + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return new EvaluationValue(operands[0].getBooleanValue() && operands[1].getBooleanValue()); + } + } + + @InfixOperator(precedence = OPERATOR_PRECEDENCE_OR) + static class OrOperator extends AbstractOperator { + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return new EvaluationValue(operands[0].getBooleanValue() || operands[1].getBooleanValue()); + } + } + + @PrefixOperator(leftAssociative = false) + static class NotOperator extends AbstractOperator { + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return new EvaluationValue(!operands[0].getBooleanValue()); + } + } + + @PostfixOperator() + static class DeniedOperator extends AbstractOperator { + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return new EvaluationValue(!operands[0].getBooleanValue()); + } + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/TokenizerNumberLiteralTest.java b/src/test/java/com/ezylang/evalex/parser/TokenizerNumberLiteralTest.java new file mode 100644 index 0000000..18974c1 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/TokenizerNumberLiteralTest.java @@ -0,0 +1,73 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import com.ezylang.evalex.parser.Token.TokenType; +import org.junit.jupiter.api.Test; + +class TokenizerNumberLiteralTest extends BaseParserTest { + + @Test + void testSingleDigit() throws ParseException { + assertAllTokensParsedCorrectly("7", new Token(1, "7", TokenType.NUMBER_LITERAL)); + } + + @Test + void testMultipleDigit() throws ParseException { + assertAllTokensParsedCorrectly("888", new Token(1, "888", TokenType.NUMBER_LITERAL)); + } + + @Test + void testBlanks() throws ParseException { + assertAllTokensParsedCorrectly("\t 123 \r\n", new Token(3, "123", TokenType.NUMBER_LITERAL)); + } + + @Test + void testDecimal() throws ParseException { + assertAllTokensParsedCorrectly("123.834", new Token(1, "123.834", TokenType.NUMBER_LITERAL)); + } + + @Test + void testDecimalStart() throws ParseException { + assertAllTokensParsedCorrectly(".9", new Token(1, ".9", TokenType.NUMBER_LITERAL)); + } + + @Test + void testDecimalEnd() throws ParseException { + assertAllTokensParsedCorrectly("123.", new Token(1, "123.", TokenType.NUMBER_LITERAL)); + } + + @Test + void testHexNumberSimple() throws ParseException { + assertAllTokensParsedCorrectly("0x0", new Token(1, "0x0", TokenType.NUMBER_LITERAL)); + } + + @Test + void testHexNumberLong() throws ParseException { + assertAllTokensParsedCorrectly( + "0x3ABCDEF0", new Token(1, "0x3ABCDEF0", TokenType.NUMBER_LITERAL)); + assertAllTokensParsedCorrectly( + " \t0x3abcdefAbcdef09873EE ", + new Token(3, "0x3abcdefAbcdef09873EE", TokenType.NUMBER_LITERAL)); + } + + @Test + void testHexNumberBlank() throws ParseException { + assertAllTokensParsedCorrectly( + " \t0x3abcdefAbcdef09873EE ", + new Token(3, "0x3abcdefAbcdef09873EE", TokenType.NUMBER_LITERAL)); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/TokenizerOperatorSeparationTest.java b/src/test/java/com/ezylang/evalex/parser/TokenizerOperatorSeparationTest.java new file mode 100644 index 0000000..431aabf --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/TokenizerOperatorSeparationTest.java @@ -0,0 +1,98 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import com.ezylang.evalex.parser.Token.TokenType; +import org.junit.jupiter.api.Test; + +class TokenizerOperatorSeparationTest extends BaseParserTest { + + @Test + void testInfixPrefix() throws ParseException { + assertAllTokensParsedCorrectly( + "2+-3", + new Token(1, "2", TokenType.NUMBER_LITERAL), + new Token(2, "+", TokenType.INFIX_OPERATOR), + new Token(3, "-", TokenType.PREFIX_OPERATOR), + new Token(4, "3", TokenType.NUMBER_LITERAL)); + } + + @Test + void testPostfixInfix() throws ParseException { + assertAllTokensParsedCorrectly( + "a++-3", + new Token(1, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(2, "++", TokenType.POSTFIX_OPERATOR), + new Token(4, "-", TokenType.INFIX_OPERATOR), + new Token(5, "3", TokenType.NUMBER_LITERAL)); + } + + @Test + void testPostfixInfixPrefix() throws ParseException { + assertAllTokensParsedCorrectly( + "a++--3", + new Token(1, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(2, "++", TokenType.POSTFIX_OPERATOR), + new Token(4, "-", TokenType.INFIX_OPERATOR), + new Token(5, "-", TokenType.PREFIX_OPERATOR), + new Token(6, "3", TokenType.NUMBER_LITERAL)); + } + + @Test + void testPrefixPrefix() throws ParseException { + assertAllTokensParsedCorrectly( + "!-2", + new Token(1, "!", TokenType.PREFIX_OPERATOR), + new Token(2, "-", TokenType.PREFIX_OPERATOR), + new Token(3, "2", TokenType.NUMBER_LITERAL)); + } + + @Test + void testEqualsEqualsNumbers() throws ParseException { + assertAllTokensParsedCorrectly( + "3==3", + new Token(1, "3", TokenType.NUMBER_LITERAL), + new Token(2, "==", TokenType.INFIX_OPERATOR), + new Token(4, "3", TokenType.NUMBER_LITERAL)); + } + + @Test + void testEqualsEqualsVariables() throws ParseException { + assertAllTokensParsedCorrectly( + "a==b", + new Token(1, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(2, "==", TokenType.INFIX_OPERATOR), + new Token(4, "b", TokenType.VARIABLE_OR_CONSTANT)); + } + + @Test + void testEqualsNumbers() throws ParseException { + assertAllTokensParsedCorrectly( + "3=3", + new Token(1, "3", TokenType.NUMBER_LITERAL), + new Token(2, "=", TokenType.INFIX_OPERATOR), + new Token(3, "3", TokenType.NUMBER_LITERAL)); + } + + @Test + void testEqualsVariables() throws ParseException { + assertAllTokensParsedCorrectly( + "a=b", + new Token(1, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(2, "=", TokenType.INFIX_OPERATOR), + new Token(3, "b", TokenType.VARIABLE_OR_CONSTANT)); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/TokenizerPrefixPostfixTest.java b/src/test/java/com/ezylang/evalex/parser/TokenizerPrefixPostfixTest.java new file mode 100644 index 0000000..41f930c --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/TokenizerPrefixPostfixTest.java @@ -0,0 +1,120 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.parser.Token.TokenType; +import org.junit.jupiter.api.Test; + +class TokenizerPrefixPostfixTest extends BaseParserTest { + + @Test + void testPrefixSingle() throws ParseException { + assertAllTokensParsedCorrectly( + "++a", + new Token(1, "++", TokenType.PREFIX_OPERATOR), + new Token(3, "a", TokenType.VARIABLE_OR_CONSTANT)); + } + + @Test + void testPostfixSingle() throws ParseException { + assertAllTokensParsedCorrectly( + "a++", + new Token(1, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(2, "++", TokenType.POSTFIX_OPERATOR)); + } + + @Test + void testPostfixAsPrefixThrowsException() { + assertThatThrownBy(new Tokenizer("?a", configuration)::parse) + .isInstanceOf(ParseException.class) + .hasMessage("Undefined operator '?'"); + } + + @Test + void testPrefixAndPostfix() throws ParseException { + // note: if this is supported, depends on the operator and what type it expects as operand + assertAllTokensParsedCorrectly( + "++a++", + new Token(1, "++", TokenType.PREFIX_OPERATOR), + new Token(3, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(4, "++", TokenType.POSTFIX_OPERATOR)); + } + + @Test + void testPrefixWithInfixAndPostfix() throws ParseException { + assertAllTokensParsedCorrectly( + "++a+a++", + new Token(1, "++", TokenType.PREFIX_OPERATOR), + new Token(3, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(4, "+", TokenType.INFIX_OPERATOR), + new Token(5, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(6, "++", TokenType.POSTFIX_OPERATOR)); + } + + @Test + void testPrefixWithBraces() throws ParseException { + assertAllTokensParsedCorrectly( + "(++a)+(a++)", + new Token(1, "(", TokenType.BRACE_OPEN), + new Token(2, "++", TokenType.PREFIX_OPERATOR), + new Token(4, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(5, ")", TokenType.BRACE_CLOSE), + new Token(6, "+", TokenType.INFIX_OPERATOR), + new Token(7, "(", TokenType.BRACE_OPEN), + new Token(8, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(9, "++", TokenType.POSTFIX_OPERATOR), + new Token(11, ")", TokenType.BRACE_CLOSE)); + } + + @Test + void testPrefixWithFunction() throws ParseException { + assertAllTokensParsedCorrectly( + "++MAX(++a,a++)++", + new Token(1, "++", TokenType.PREFIX_OPERATOR), + new Token(3, "MAX", TokenType.FUNCTION), + new Token(6, "(", TokenType.BRACE_OPEN), + new Token(7, "++", TokenType.PREFIX_OPERATOR), + new Token(9, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(10, ",", TokenType.COMMA), + new Token(11, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(12, "++", TokenType.POSTFIX_OPERATOR), + new Token(14, ")", TokenType.BRACE_CLOSE), + new Token(15, "++", TokenType.POSTFIX_OPERATOR)); + } + + @Test + void testPrefixWithStringLiteral() throws ParseException { + assertAllTokensParsedCorrectly( + "++\"hello\"++", + new Token(1, "++", TokenType.PREFIX_OPERATOR), + new Token(3, "hello", TokenType.STRING_LITERAL), + new Token(10, "++", TokenType.POSTFIX_OPERATOR)); + } + + @Test + void testPrefixWithUnaryAndBinary() throws ParseException { + assertAllTokensParsedCorrectly( + "-a - -b++", + new Token(1, "-", TokenType.PREFIX_OPERATOR), + new Token(2, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(4, "-", TokenType.INFIX_OPERATOR), + new Token(6, "-", TokenType.PREFIX_OPERATOR), + new Token(7, "b", TokenType.VARIABLE_OR_CONSTANT), + new Token(8, "++", TokenType.POSTFIX_OPERATOR)); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/TokenizerStringLiteralTest.java b/src/test/java/com/ezylang/evalex/parser/TokenizerStringLiteralTest.java new file mode 100644 index 0000000..80270da --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/TokenizerStringLiteralTest.java @@ -0,0 +1,67 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.parser.Token.TokenType; +import org.junit.jupiter.api.Test; + +class TokenizerStringLiteralTest extends BaseParserTest { + + @Test + void testSimpleQuote() throws ParseException { + assertAllTokensParsedCorrectly( + "\"Hello, World\"", new Token(1, "Hello, World", TokenType.STRING_LITERAL)); + } + + @Test + void testSimpleQuoteLeadingBlanks() throws ParseException { + assertAllTokensParsedCorrectly( + " \t\n \"Hello, World\"", new Token(6, "Hello, World", TokenType.STRING_LITERAL)); + } + + @Test + void testSimpleQuoteTrailingBlanks() throws ParseException { + assertAllTokensParsedCorrectly( + "\"Hello, World\" \t\n ", new Token(1, "Hello, World", TokenType.STRING_LITERAL)); + } + + @Test + void testSimpleQuoteOperation() throws ParseException { + assertAllTokensParsedCorrectly( + "\"Hello\" + \" \" + \"World\"", + new Token(1, "Hello", TokenType.STRING_LITERAL), + new Token(9, "+", TokenType.INFIX_OPERATOR), + new Token(11, " ", TokenType.STRING_LITERAL), + new Token(15, "+", TokenType.INFIX_OPERATOR), + new Token(17, "World", TokenType.STRING_LITERAL)); + } + + @Test + void testErrorUnmatchedQuoteStart() { + assertThatThrownBy(() -> new Tokenizer("\"hello", configuration).parse()) + .isInstanceOf(ParseException.class) + .hasMessage("Closing quote not found"); + } + + @Test + void testErrorUnmatchedQuoteOffset() { + assertThatThrownBy(() -> new Tokenizer("test \"hello", configuration).parse()) + .isInstanceOf(ParseException.class) + .hasMessage("Closing quote not found"); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/TokenizerStructureTest.java b/src/test/java/com/ezylang/evalex/parser/TokenizerStructureTest.java new file mode 100644 index 0000000..ea63232 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/TokenizerStructureTest.java @@ -0,0 +1,55 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.parser.Token.TokenType; +import org.junit.jupiter.api.Test; + +class TokenizerStructureTest extends BaseParserTest { + + @Test + void testStructureSimple() throws ParseException { + assertAllTokensParsedCorrectly( + "a.b", + new Token(1, "a", TokenType.VARIABLE_OR_CONSTANT), + new Token(2, ".", TokenType.STRUCTURE_SEPARATOR), + new Token(3, "b", TokenType.VARIABLE_OR_CONSTANT)); + } + + @Test + void testStructureSeparatorNotAllowedBegin() { + assertThatThrownBy(() -> new Tokenizer(".", configuration).parse()) + .isEqualTo(new ParseException(1, 1, ".", "Structure separator not allowed here")); + } + + @Test + void testStructureSeparatorNotAllowedOperator() { + assertThatThrownBy(() -> new Tokenizer("-.", configuration).parse()) + .isEqualTo(new ParseException(2, 2, ".", "Structure separator not allowed here")); + } + + @Test + void testStructureNotAllowed() { + ExpressionConfiguration config = + ExpressionConfiguration.builder().structuresAllowed(false).build(); + + assertThatThrownBy(() -> new Tokenizer("a.b", config).parse()) + .isEqualTo(new ParseException(2, 2, ".", "Undefined operator '.'")); + } +} diff --git a/src/test/java/com/ezylang/evalex/parser/TokenizerVariableNameTest.java b/src/test/java/com/ezylang/evalex/parser/TokenizerVariableNameTest.java new file mode 100644 index 0000000..6bfb54e --- /dev/null +++ b/src/test/java/com/ezylang/evalex/parser/TokenizerVariableNameTest.java @@ -0,0 +1,59 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.parser; + +import com.ezylang.evalex.parser.Token.TokenType; +import org.junit.jupiter.api.Test; + +class TokenizerVariableNameTest extends BaseParserTest { + + @Test + void testSimple() throws ParseException { + assertAllTokensParsedCorrectly("a", new Token(1, "a", TokenType.VARIABLE_OR_CONSTANT)); + } + + @Test + void testEndWithNumber() throws ParseException { + assertAllTokensParsedCorrectly("var1", new Token(1, "var1", TokenType.VARIABLE_OR_CONSTANT)); + } + + @Test + void testContainsNumber() throws ParseException { + assertAllTokensParsedCorrectly( + "var2test", new Token(1, "var2test", TokenType.VARIABLE_OR_CONSTANT)); + } + + @Test + void testUnderscore() throws ParseException { + assertAllTokensParsedCorrectly( + "_var_2_", new Token(1, "_var_2_", TokenType.VARIABLE_OR_CONSTANT)); + } + + @Test + void testUmlaut() throws ParseException { + assertAllTokensParsedCorrectly("Grün", new Token(1, "Grün", TokenType.VARIABLE_OR_CONSTANT)); + assertAllTokensParsedCorrectly( + "olá_enchanté_γεια_σας", + new Token(1, "olá_enchanté_γεια_σας", TokenType.VARIABLE_OR_CONSTANT)); + } + + @Test + void testSpecialAlphabetical() throws ParseException { + assertAllTokensParsedCorrectly( + "olá_enchanté_γεια_σας", + new Token(1, "olá_enchanté_γεια_σας", TokenType.VARIABLE_OR_CONSTANT)); + } +} diff --git a/src/test/java/com/udojava/evalex/TestBooleans.java b/src/test/java/com/udojava/evalex/TestBooleans.java deleted file mode 100644 index 791cecb..0000000 --- a/src/test/java/com/udojava/evalex/TestBooleans.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.udojava.evalex; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.util.Iterator; -import org.junit.Test; - - -public class TestBooleans { - - private void assertToken(String surface, Expression.TokenType type, Expression.Token actual) { - assertEquals(surface, actual.surface); - assertEquals(type, actual.type); - } - - @Test - public void testIsBoolean() { - Expression e = new Expression("1==1"); - assertTrue(e.isBoolean()); - - e = new Expression("a==b").with("a", "1").and("b", "2"); - assertTrue(e.isBoolean()); - - e = new Expression("(1==1)||(c==a+b)"); - assertTrue(e.isBoolean()); - - e = new Expression("(z+z==x-y)||(c==a+b)"); - assertTrue(e.isBoolean()); - - e = new Expression("NOT(a+b)"); - assertTrue(e.isBoolean()); - - e = new Expression("a+b"); - assertFalse(e.isBoolean()); - - e = new Expression("(a==b)+(b==c)"); - assertFalse(e.isBoolean()); - - e = new Expression("SQRT(2)"); - assertFalse(e.isBoolean()); - - e = new Expression("SQRT(2) == SQRT(b)"); - assertTrue(e.isBoolean()); - - e = new Expression("IF(a==b,x+y,x-y)"); - assertFalse(e.isBoolean()); - - e = new Expression("IF(a==b,x==y,a==b)"); - assertTrue(e.isBoolean()); - } - - @Test - public void testAndTokenizer() { - Expression e = new Expression("1&&0"); - Iterator i = e.getExpressionTokenizer(); - - assertToken("1", Expression.TokenType.LITERAL, i.next()); - assertToken("&&", Expression.TokenType.OPERATOR, i.next()); - assertToken("0", Expression.TokenType.LITERAL, i.next()); - } - - @Test - public void testAndRPN() { - assertEquals("1 0 &&", new Expression("1&&0").toRPN()); - } - - @Test - public void testAndEval() { - assertEquals("0", new Expression("1&&0").eval().toString()); - assertEquals("1", new Expression("1&&1").eval().toString()); - assertEquals("0", new Expression("0&&0").eval().toString()); - assertEquals("0", new Expression("0&&1").eval().toString()); - } - - @Test - public void testOrEval() { - assertEquals("1", new Expression("1||0").eval().toString()); - assertEquals("1", new Expression("1||1").eval().toString()); - assertEquals("0", new Expression("0||0").eval().toString()); - assertEquals("1", new Expression("0||1").eval().toString()); - } - - @Test - public void testCompare() { - assertEquals("1", new Expression("2>1").eval().toString()); - assertEquals("0", new Expression("2<1").eval().toString()); - assertEquals("0", new Expression("1>2").eval().toString()); - assertEquals("1", new Expression("1<2").eval().toString()); - assertEquals("0", new Expression("1=2").eval().toString()); - assertEquals("1", new Expression("1=1").eval().toString()); - assertEquals("1", new Expression("1>=1").eval().toString()); - assertEquals("1", new Expression("1.1>=1").eval().toString()); - assertEquals("0", new Expression("1>=2").eval().toString()); - assertEquals("1", new Expression("1<=1").eval().toString()); - assertEquals("0", new Expression("1.1<=1").eval().toString()); - assertEquals("1", new Expression("1<=2").eval().toString()); - assertEquals("0", new Expression("1=2").eval().toString()); - assertEquals("1", new Expression("1=1").eval().toString()); - assertEquals("1", new Expression("1!=2").eval().toString()); - assertEquals("0", new Expression("1!=1").eval().toString()); - } - - @Test - public void testCompareCombined() { - assertEquals("1", new Expression("(2>1)||(1=0)").eval().toString()); - assertEquals("0", new Expression("(2>3)||(1=0)").eval().toString()); - assertEquals("1", new Expression("(2>3)||(1=0)||(1&&1)").eval().toString()); - } - - @Test - public void testMixed() { - assertEquals("0", new Expression("1.5 * 7 = 3").eval().toString()); - assertEquals("1", new Expression("1.5 * 7 = 10.5").eval().toString()); - } - - @Test - public void testNot() { - assertEquals("0", new Expression("not(1)").eval().toString()); - assertEquals("1", new Expression("not(0)").eval().toString()); - assertEquals("1", new Expression("not(1.5 * 7 = 3)").eval().toString()); - assertEquals("0", new Expression("not(1.5 * 7 = 10.5)").eval().toString()); - } - - @Test - public void testConstants() { - assertEquals("1", new Expression("TRUE!=FALSE").eval().toString()); - assertEquals("0", new Expression("TRUE==2").eval().toString()); - assertEquals("1", new Expression("NOT(TRUE)==FALSE").eval().toString()); - assertEquals("1", new Expression("NOT(FALSE)==TRUE").eval().toString()); - assertEquals("0", new Expression("TRUE && FALSE").eval().toString()); - assertEquals("1", new Expression("TRUE || FALSE").eval().toString()); - } - - @Test - public void testIf() { - assertEquals("5", new Expression("if(TRUE, 5, 3)").eval().toString()); - assertEquals("3", new Expression("IF(FALSE, 5, 3)").eval().toString()); - assertEquals("5.35", new Expression("If(2, 5.35, 3)").eval().toString()); - } - - @Test - public void testDecimals() { - assertEquals("0", new Expression("if(0.0, 1, 0)").eval().toPlainString()); - assertEquals("0", new Expression("0.0 || 0.0").eval().toPlainString()); - assertEquals("1", new Expression("not(0.0)").eval().toPlainString()); - assertEquals("0", new Expression("0.0 && 0.0").eval().toPlainString()); - } -} diff --git a/src/test/java/com/udojava/evalex/TestCaseInsensitive.java b/src/test/java/com/udojava/evalex/TestCaseInsensitive.java deleted file mode 100644 index 61a13ca..0000000 --- a/src/test/java/com/udojava/evalex/TestCaseInsensitive.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.udojava.evalex; - -import java.math.BigDecimal; -import java.util.List; -import org.junit.Assert; -import org.junit.Test; - -/** - * Created by showdown on 2/28/2016 at 3:29 PM. Project EvalEx - */ -public class TestCaseInsensitive { - - @Test - public void testVariableIsCaseInsensitive() { - - Expression expression = new Expression("a"); - expression.setVariable("A", new BigDecimal(20)); - Assert.assertEquals(20, expression.eval().intValue()); - - expression = new Expression("a + B"); - expression.setVariable("A", new BigDecimal(10)); - expression.setVariable("b", new BigDecimal(10)); - Assert.assertEquals(20, expression.eval().intValue()); - - expression = new Expression("a+B"); - expression.setVariable("A", "c+d"); - expression.setVariable("b", new BigDecimal(10)); - expression.setVariable("C", new BigDecimal(5)); - expression.setVariable("d", new BigDecimal(5)); - Assert.assertEquals(20, expression.eval().intValue()); - } - - @Test - public void testFunctionCaseInsensitive() { - - Expression expression = new Expression("a+testsum(1,3)"); - expression.setVariable("A", new BigDecimal(1)); - expression.addFunction(expression.new Function("testSum", -1) { - @Override - public BigDecimal eval(List parameters) { - BigDecimal value = null; - for (BigDecimal d : parameters) { - value = value == null ? d : value.add(d); - } - return value; - } - }); - - Assert.assertEquals(expression.eval(), new BigDecimal(5)); - - } -} diff --git a/src/test/java/com/udojava/evalex/TestConstants.java b/src/test/java/com/udojava/evalex/TestConstants.java deleted file mode 100644 index bbda797..0000000 --- a/src/test/java/com/udojava/evalex/TestConstants.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.udojava.evalex; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; - -import java.math.BigDecimal; -import java.math.MathContext; -import java.util.Arrays; -import java.util.Collection; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -@RunWith(Parameterized.class) -public class TestConstants { - private final String constantName; - private final BigDecimal constantValue; - - public TestConstants(String constantName, BigDecimal value) { - this.constantName = constantName; - this.constantValue = value; - } - - @Parameters - public static Collection data() { - return Arrays.asList( - new Object[]{"e", Expression.e}, - new Object[]{"PI", Expression.PI}, - new Object[]{"TRUE", BigDecimal.ONE}, - new Object[]{"FALSE", BigDecimal.ZERO}, - new Object[]{"NULL", null} - ); - } - - @Test - public void testEvaluateConstants() { - assertEquals( - constantValue, - new Expression(constantName) - .setPrecision(MathContext.UNLIMITED.getPrecision()) - .eval() - ); - } - - @Test - public void testConstantsAreNotVars() { - assertTrue(new Expression(constantName).getUsedVariables().isEmpty()); - } -} diff --git a/src/test/java/com/udojava/evalex/TestContinuousUnary.java b/src/test/java/com/udojava/evalex/TestContinuousUnary.java deleted file mode 100644 index e3f9905..0000000 --- a/src/test/java/com/udojava/evalex/TestContinuousUnary.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.udojava.evalex; - -import org.junit.Assert; -import org.junit.Test; - -public class TestContinuousUnary { - - @Test - public void testContinuousUnary() { - - Expression expression = new Expression("++1"); - Assert.assertEquals(1, expression.eval().intValue()); - - expression = new Expression("--1"); - Assert.assertEquals(1, expression.eval().intValue()); - - expression = new Expression("+-1"); - Assert.assertEquals(-1, expression.eval().intValue()); - - expression = new Expression("-+1"); - Assert.assertEquals(-1, expression.eval().intValue()); - - expression = new Expression("1-+1"); - Assert.assertEquals(0, expression.eval().intValue()); - - expression = new Expression("-+---+++--++-1"); - Assert.assertEquals(-1, expression.eval().intValue()); - - expression = new Expression("1--++++---2+-+----1"); - Assert.assertEquals(-2, expression.eval().intValue()); - } -} diff --git a/src/test/java/com/udojava/evalex/TestCustoms.java b/src/test/java/com/udojava/evalex/TestCustoms.java deleted file mode 100644 index 039753c..0000000 --- a/src/test/java/com/udojava/evalex/TestCustoms.java +++ /dev/null @@ -1,296 +0,0 @@ -package com.udojava.evalex; - -import static org.junit.Assert.assertEquals; - -import com.udojava.evalex.Expression.LazyNumber; -import java.math.BigDecimal; -import java.math.MathContext; -import java.util.List; -import org.junit.Test; - -public class TestCustoms { - - @Test - public void testCustomOperator() { - Expression e = new Expression("2.1234 >> 2"); - - e.addOperator(e.new Operator(">>", 30, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - return v1.movePointRight(v2.toBigInteger().intValue()); - } - }); - - assertEquals("212.34", e.eval().toPlainString()); - } - - @Test - public void testCustomFunction() { - Expression e = new Expression("2 * average(12,4,8)"); - e.addFunction(new AbstractFunction("average", 3) { - @Override - public BigDecimal eval(List parameters) { - BigDecimal sum = parameters.get(0).add(parameters.get(1)).add(parameters.get(2)); - return sum.divide(new BigDecimal(3), MathContext.DECIMAL32); - } - }); - - assertEquals("16", e.eval().toPlainString()); - } - - @Test - public void testCustomFunctionInstanceClass() { - Expression e = new Expression("2 * average(12,4,8)"); - e.addFunction(e.new Function("average", 3) { - @Override - public BigDecimal eval(List parameters) { - BigDecimal sum = parameters.get(0).add(parameters.get(1)).add(parameters.get(2)); - return sum.divide(new BigDecimal(3), MathContext.DECIMAL32); - } - }); - - assertEquals("16", e.eval().toPlainString()); - } - - @Test - public void testCustomFunctionVariableParameters() { - Expression e = new Expression("2 * average(12,4,8,2,9)"); - e.addFunction(new AbstractFunction("average", -1) { - @Override - public BigDecimal eval(List parameters) { - BigDecimal sum = new BigDecimal(0); - for (BigDecimal parameter : parameters) { - sum = sum.add(parameter); - } - return sum.divide(new BigDecimal(parameters.size()), MathContext.DECIMAL32); - } - }); - - assertEquals("14", e.eval().toPlainString()); - } - - @Test - public void testCustomFunctionVariableParametersInstanceClass() { - Expression e = new Expression("2 * average(12,4,8,2,9)"); - e.addFunction(e.new Function("average", -1) { - @Override - public BigDecimal eval(List parameters) { - BigDecimal sum = new BigDecimal(0); - for (BigDecimal parameter : parameters) { - sum = sum.add(parameter); - } - return sum.divide(new BigDecimal(parameters.size()), MathContext.DECIMAL32); - } - }); - - assertEquals("14", e.eval().toPlainString()); - } - - @Test - public void testCustomFunctionStringParameters() { - Expression e = new Expression("STREQ(\"test\", \"test2\")"); - e.addLazyFunction(e.new LazyFunction("STREQ", 2) { - private LazyNumber ZERO = new LazyNumber() { - public BigDecimal eval() { - return BigDecimal.ZERO; - } - - public String getString() { - return "0"; - } - }; - - private LazyNumber ONE = new LazyNumber() { - public BigDecimal eval() { - return BigDecimal.ONE; - } - - public String getString() { - return null; - } - }; - - @Override - public LazyNumber lazyEval(List lazyParams) { - if (lazyParams.get(0).getString().equals(lazyParams.get(1).getString())) { - return ONE; - } - return ZERO; - } - }); - - assertEquals("0", e.eval().toPlainString()); - } - - @Test - public void testCustomFunctionStringParametersInstanceClass() { - Expression e = new Expression("STREQ(\"test\", \"test2\")"); - e.addLazyFunction(e.new LazyFunction("STREQ", 2) { - private LazyNumber ZERO = new LazyNumber() { - public BigDecimal eval() { - return BigDecimal.ZERO; - } - - public String getString() { - return "0"; - } - }; - - private LazyNumber ONE = new LazyNumber() { - public BigDecimal eval() { - return BigDecimal.ONE; - } - - public String getString() { - return null; - } - }; - - @Override - public LazyNumber lazyEval(List lazyParams) { - if (lazyParams.get(0).getString().equals(lazyParams.get(1).getString())) { - return ONE; - } - return ZERO; - } - }); - - assertEquals("0", e.eval().toPlainString()); - } - - @Test - public void testCustomFunctionBoolean() { - AbstractLazyFunction stringCompare = new AbstractLazyFunction("STREQ", 2, true) { - private LazyNumber ZERO = new LazyNumber() { - public BigDecimal eval() { - return BigDecimal.ZERO; - } - - public String getString() { - return "0"; - } - }; - - private LazyNumber ONE = new LazyNumber() { - public BigDecimal eval() { - return BigDecimal.ONE; - } - - public String getString() { - return null; - } - }; - - @Override - public LazyNumber lazyEval(List lazyParams) { - if (lazyParams.get(0).getString().equals(lazyParams.get(1).getString())) { - return ONE; - } - return ZERO; - } - }; - - Expression e1 = new Expression("STREQ(\"test\", \"test\")"); - e1.addLazyFunction(stringCompare); - assertEquals("1", e1.eval().toPlainString()); - - Expression e2 = new Expression("STREQ(\"test\", \"test2\")"); - e2.addLazyFunction(stringCompare); - assertEquals("0", e2.eval().toPlainString()); - } - - @Test - public void testUnary() { - Expression exp = new Expression("~23"); - - exp.addOperator(new AbstractUnaryOperator("~", 60, false) { - @Override - public BigDecimal evalUnary(BigDecimal bigDecimal) { - return bigDecimal; - } - }); - assertEquals("23", exp.eval().toPlainString()); - } - - @Test - public void testUnaryPostfix() { - Expression exp = new Expression("1+4!"); - - exp.addOperator(new AbstractOperator("!", 61, true, false, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - if (!v1.remainder(BigDecimal.ONE).equals(BigDecimal.ZERO)) { - throw new ArithmeticException("Operand must be an integer"); - } - BigDecimal factorial = v1; - v1 = v1.subtract(BigDecimal.ONE); - if (factorial.compareTo(BigDecimal.ZERO) == 0 || factorial.compareTo(BigDecimal.ONE) == 0) { - return BigDecimal.ONE; - } else { - while (v1.compareTo(BigDecimal.ONE) > 0) { - factorial = factorial.multiply(v1); - v1 = v1.subtract(BigDecimal.ONE); - } - return factorial; - } - } - }); - assertEquals("25", exp.eval().toPlainString()); - } - - @Test - public void testCustomOperatorAnd() { - Expression e = new Expression("1 == 1 AND 2 == 2"); - - e.addOperator(e.new Operator("AND", Expression.OPERATOR_PRECEDENCE_AND, false, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - boolean b1 = v1.compareTo(BigDecimal.ZERO) != 0; - - if (!b1) { - return BigDecimal.ZERO; - } - - boolean b2 = v2.compareTo(BigDecimal.ZERO) != 0; - return b2 ? BigDecimal.ONE : BigDecimal.ZERO; - } - }); - - assertEquals("1", e.eval().toPlainString()); - } - - @Test - public void testCustomFunctionWithStringParams() { - Expression e = new Expression("INDEXOF(STRING1, STRING2)"); - e.addLazyFunction(new AbstractLazyFunction("INDEXOF", 2) { - @Override - public LazyNumber lazyEval(List lazyParams) { - String st1 = lazyParams.get(0).getString(); - String st2 = lazyParams.get(1).getString(); - final int index = st1.indexOf(st2); - - return new LazyNumber() { - @Override - public BigDecimal eval() { - return new BigDecimal(index); - } - - @Override - public String getString() { - return Integer.toString(index); - } - }; - } - }); - - e.setVariable("STRING1", "The quick brown fox"); - e.setVariable("STRING2", "The"); - - assertEquals("0", e.eval().toPlainString()); - - e.setVariable("STRING1", "The quick brown fox"); - e.setVariable("STRING2", "brown"); - - assertEquals("10", e.eval().toPlainString()); - } -} diff --git a/src/test/java/com/udojava/evalex/TestEval.java b/src/test/java/com/udojava/evalex/TestEval.java deleted file mode 100644 index 1125948..0000000 --- a/src/test/java/com/udojava/evalex/TestEval.java +++ /dev/null @@ -1,572 +0,0 @@ -package com.udojava.evalex; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertNull; - -import com.udojava.evalex.Expression.ExpressionException; -import java.math.BigDecimal; -import java.math.MathContext; -import java.math.RoundingMode; -import org.junit.Test; - - -public class TestEval { - - @Test - public void testsinAB() { - String err = ""; - try { - Expression expression = new Expression("sin(a+x)"); - expression.eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - - assertEquals("Unknown operator or function: a", err); - } - - @Test - public void testInvalidExpressions1() { - String err = ""; - try { - Expression expression = new Expression("12 18 2"); - expression.eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - - assertEquals("Missing operator at character position 3", err); - } - - @Test - public void testInvalidExpressions3() { - String err = ""; - try { - Expression expression = new Expression("12 + * 18"); - expression.eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - - assertEquals("Unknown unary operator * at character position 6", err); - } - - @Test - public void testInvalidExpressions4() { - String err = ""; - try { - Expression expression = new Expression(""); - expression.eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - - assertEquals("Empty expression", err); - } - - @Test - public void testInvalidExpressions5() { - String err = ""; - try { - Expression expression = new Expression("1 1+2/"); - expression.toRPN(); - expression.eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - - assertEquals("Missing operator at character position 2", err); - } - - @Test - public void testBrackets() { - assertEquals("3", new Expression("(1+2)").eval().toPlainString()); - assertEquals("3", new Expression("((1+2))").eval().toPlainString()); - assertEquals("3", new Expression("(((1+2)))").eval().toPlainString()); - assertEquals("9", new Expression("(1+2)*(1+2)").eval().toPlainString()); - assertEquals("10", new Expression("(1+2)*(1+2)+1").eval().toPlainString()); - assertEquals("12", new Expression("(1+2)*((1+2)+1)").eval().toPlainString()); - } - - @Test(expected = RuntimeException.class) - public void testUnknow1() { - assertEquals("", new Expression("7#9").eval().toPlainString()); - } - - @Test(expected = RuntimeException.class) - public void testUnknow2() { - assertEquals("", new Expression("123.6*-9.8-7#9").eval().toPlainString()); - } - - @Test - public void testSimple() { - assertEquals("3", new Expression("1+2").eval().toPlainString()); - assertEquals("2", new Expression("4/2").eval().toPlainString()); - assertEquals("5", new Expression("3+4/2").eval().toPlainString()); - assertEquals("3.5", new Expression("(3+4)/2").eval().toPlainString()); - assertEquals("7.98", new Expression("4.2*1.9").eval().toPlainString()); - assertEquals("2", new Expression("8%3").eval().toPlainString()); - assertEquals("0", new Expression("8%2").eval().toPlainString()); - assertEquals("0.2", new Expression("2*.1").eval().toPlainString()); - } - - @Test - public void testUnaryMinus() { - assertEquals("-3", new Expression("-3").eval().toPlainString()); - assertEquals("-2", new Expression("-SQRT(4)").eval().toPlainString()); - assertEquals("-12", new Expression("-(5*3+(+10-13))").eval().toPlainString()); - assertEquals("-2.75", new Expression("-2+3/4*-1").eval().toPlainString()); - assertEquals("9", new Expression("-3^2").eval().toPlainString()); - assertEquals("0.5", new Expression("4^-0.5").eval().toPlainString()); - assertEquals("-1.25", new Expression("-2+3/4").eval().toPlainString()); - assertEquals("-1", new Expression("-(3+-4*-1/-2)").eval().toPlainString()); - assertEquals("1.8", new Expression("2+-.2").eval().toPlainString()); - } - - @Test - public void testUnaryPlus() { - assertEquals("3", new Expression("+3").eval().toPlainString()); - assertEquals("4", new Expression("+(3-1+2)").eval().toPlainString()); - assertEquals("4", new Expression("+(3-(+1)+2)").eval().toPlainString()); - assertEquals("9", new Expression("+3^2").eval().toPlainString()); - } - - @Test - public void testPow() { - assertEquals("16", new Expression("2^4").eval().toPlainString()); - assertEquals("256", new Expression("2^8").eval().toPlainString()); - assertEquals("9", new Expression("3^2").eval().toPlainString()); - assertEquals("6.25", new Expression("2.5^2").eval().toPlainString()); - assertEquals("28.34045", new Expression("2.6^3.5").eval().toPlainString()); - } - - @Test - public void testSqrt() { - assertEquals("4", new Expression("SQRT(16)").eval().toPlainString()); - assertEquals("1.4142135", new Expression("SQRT(2)").eval().toPlainString()); - assertEquals( - "1.41421356237309504880168872420969807856967187537694807317667973799073247846210703885038753432764157273501384623091229702492483605", - new Expression("SQRT(2)").setPrecision(128).eval().toPlainString()); - assertEquals("2.2360679", new Expression("SQRT(5)").eval().toPlainString()); - assertEquals("99.3730345", new Expression("SQRT(9875)").eval().toPlainString()); - assertEquals("2.3558437", new Expression("SQRT(5.55)").eval().toPlainString()); - assertEquals("0", new Expression("SQRT(0)").eval().toPlainString()); - } - - @Test - public void testFunctions() { - assertNotSame("1.5", new Expression("Random()").eval().toPlainString()); - assertEquals("0.400349", new Expression("SIN(23.6)").eval().toPlainString()); - assertEquals("8", new Expression("MAX(-7,8)").eval().toPlainString()); - assertEquals("5", new Expression("MAX(3,max(4,5))").eval().toPlainString()); - assertEquals("9.6", - new Expression("MAX(3,max(MAX(9.6,-4.2),Min(5,9)))").eval().toPlainString()); - assertEquals("2.302585", new Expression("LOG(10)").eval().toPlainString()); - } - - @Test - public void testExpectedParameterNumbers() { - String err = ""; - try { - Expression expression = new Expression("Random(1)"); - expression.eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - assertEquals("Function Random expected 0 parameters, got 1", err); - - try { - Expression expression = new Expression("SIN(1, 6)"); - expression.eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - assertEquals("Function SIN expected 1 parameters, got 2", err); - } - - @Test - public void testVariableParameterNumbers() { - String err = ""; - try { - Expression expression = new Expression("min()"); - expression.eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - assertEquals("MIN requires at least one parameter", err); - - assertEquals("1", new Expression("min(1)").eval().toPlainString()); - assertEquals("1", new Expression("min(1, 2)").eval().toPlainString()); - assertEquals("1", new Expression("min(1, 2, 3)").eval().toPlainString()); - assertEquals("3", new Expression("max(3, 2, 1)").eval().toPlainString()); - assertEquals("9", new Expression("max(3, 2, 1, 4, 5, 6, 7, 8, 9, 0)").eval().toPlainString()); - } - - @Test - public void testOrphanedOperators() { - String err = ""; - try { - new Expression("/").eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - assertEquals("Unknown unary operator / at character position 1", err); - - err = ""; - try { - new Expression("3/").eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - assertEquals("Missing parameter(s) for operator /", err); - - err = ""; - try { - new Expression("/3").eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - assertEquals("Unknown unary operator / at character position 1", err); - - err = ""; - try { - new Expression("SIN(MAX(23,45,12))/").eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - assertEquals("Missing parameter(s) for operator /", err); - - } - - @Test(expected = ExpressionException.class) - public void closeParenAtStartCausesExpressionException() { - new Expression("(").eval(); - } - - @Test - public void testOrphanedOperatorsInFunctionParameters() { - String err = ""; - try { - new Expression("min(/)").eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - assertEquals("Unknown unary operator / at character position 5", err); - - err = ""; - try { - new Expression("min(3/)").eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - assertEquals("Missing parameter(s) for operator / at character position 5", err); - - err = ""; - try { - new Expression("min(/3)").eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - assertEquals("Unknown unary operator / at character position 5", err); - - err = ""; - try { - new Expression("SIN(MAX(23,45,12,23.6/))").eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - assertEquals("Missing parameter(s) for operator / at character position 21", err); - - err = ""; - try { - new Expression("SIN(MAX(23,45,12/,23.6))").eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - assertEquals("Missing parameter(s) for operator / at character position 16", err); - - err = ""; - try { - new Expression("SIN(MAX(23,45,>=12,23.6))").eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - assertEquals("Unknown unary operator >= at character position 15", err); - - err = ""; - try { - new Expression("SIN(MAX(>=23,45,12,23.6))").eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - assertEquals("Unknown unary operator >= at character position 9", err); - } - - @Test - public void testExtremeFunctionNesting() { - assertNotSame("1.5", new Expression("Random()").eval().toPlainString()); - assertEquals("0.0002791281", new Expression("SIN(SIN(COS(23.6)))").eval().toPlainString()); - assertEquals("-4", - new Expression("MIN(0, SIN(SIN(COS(23.6))), 0-MAX(3,4,MAX(0,SIN(1))), 10)").eval() - .toPlainString()); - } - - @Test - public void testTrigonometry() { - assertEquals("0.5", new Expression("SIN(30)").eval().toPlainString()); - assertEquals("0.8660254", new Expression("cos(30)").eval().toPlainString()); - assertEquals("0.5773503", new Expression("TAN(30)").eval().toPlainString()); - assertEquals("5343237000000", new Expression("SINH(30)").eval().toPlainString()); - assertEquals("5343237000000", new Expression("COSH(30)").eval().toPlainString()); - assertEquals("1", new Expression("TANH(30)").eval().toPlainString()); - assertEquals("0.5235988", new Expression("RAD(30)").eval().toPlainString()); - assertEquals("1718.873", new Expression("DEG(30)").eval().toPlainString()); - assertEquals("30", new Expression("atan(0.5773503)").eval().toPlainString()); - assertEquals("30", new Expression("atan2(0.5773503, 1)").eval().toPlainString()); - assertEquals("33.69007", new Expression("atan2(2, 3)").eval().toPlainString()); - assertEquals("146.3099", new Expression("atan2(2, -3)").eval().toPlainString()); - assertEquals("-146.3099", new Expression("atan2(-2, -3)").eval().toPlainString()); - assertEquals("-33.69007", new Expression("atan2(-2, 3)").eval().toPlainString()); - assertEquals("1.154701", new Expression("SEC(30)").eval().toPlainString()); - assertEquals("1.414214", new Expression("SEC(45)").eval().toPlainString()); - assertEquals("2", new Expression("SEC(60)").eval().toPlainString()); - assertEquals("3.863703", new Expression("SEC(75)").eval().toPlainString()); - assertEquals("2", new Expression("CSC(30)").eval().toPlainString()); - assertEquals("1.414214", new Expression("CSC(45)").eval().toPlainString()); - assertEquals("1.154701", new Expression("CSC(60)").eval().toPlainString()); - assertEquals("1.035276", new Expression("CSC(75)").eval().toPlainString()); - assertEquals("0.0000000000001871525", new Expression("SECH(30)").eval().toPlainString()); - assertEquals("0.00000000000000000005725037", new Expression("SECH(45)").eval().toPlainString()); - assertEquals("0.00000000000000000000000001751302", - new Expression("SECH(60)").eval().toPlainString()); - assertEquals("0.000000000000000000000000000000005357274", - new Expression("SECH(75)").eval().toPlainString()); - assertEquals("0.0000000000001871525", new Expression("CSCH(30)").eval().toPlainString()); - assertEquals("0.00000000000000000005725037", new Expression("CSCH(45)").eval().toPlainString()); - assertEquals("0.00000000000000000000000001751302", - new Expression("CSCH(60)").eval().toPlainString()); - assertEquals("0.000000000000000000000000000000005357274", - new Expression("CSCH(75)").eval().toPlainString()); - assertEquals("1.732051", new Expression("COT(30)").eval().toPlainString()); - assertEquals("1", new Expression("COT(45)").eval().toPlainString()); - assertEquals("0.5773503", new Expression("COT(60)").eval().toPlainString()); - assertEquals("0.2679492", new Expression("COT(75)").eval().toPlainString()); - assertEquals("1.909152", new Expression("ACOT(30)").eval().toPlainString()); - assertEquals("1.27303", new Expression("ACOT(45)").eval().toPlainString()); - assertEquals("0.9548413", new Expression("ACOT(60)").eval().toPlainString()); - assertEquals("0.7638985", new Expression("ACOT(75)").eval().toPlainString()); - assertEquals("1", new Expression("COTH(30)").eval().toPlainString()); - assertEquals("1.199538", new Expression("COTH(1.2)").eval().toPlainString()); - assertEquals("1.016596", new Expression("COTH(2.4)").eval().toPlainString()); - assertEquals("4.094622", new Expression("ASINH(30)").eval().toPlainString()); - assertEquals("4.499933", new Expression("ASINH(45)").eval().toPlainString()); - assertEquals("4.787561", new Expression("ASINH(60)").eval().toPlainString()); - assertEquals("5.01068", new Expression("ASINH(75)").eval().toPlainString()); - assertEquals("0", new Expression("ACOSH(1)").eval().toPlainString()); - assertEquals("4.094067", new Expression("ACOSH(30)").eval().toPlainString()); - assertEquals("4.499686", new Expression("ACOSH(45)").eval().toPlainString()); - assertEquals("4.787422", new Expression("ACOSH(60)").eval().toPlainString()); - assertEquals("5.010591", new Expression("ACOSH(75)").eval().toPlainString()); - assertEquals("0", new Expression("ATANH(0)").eval().toPlainString()); - assertEquals("0.5493061", new Expression("ATANH(0.5)").eval().toPlainString()); - assertEquals("-0.5493061", new Expression("ATANH(-0.5)").eval().toPlainString()); - } - - @Test - public void testMinMaxAbs() { - assertEquals("3.78787", new Expression("MAX(3.78787,3.78786)").eval().toPlainString()); - assertEquals("3.78787", new Expression("max(3.78786,3.78787)").eval().toPlainString()); - assertEquals("3.78786", new Expression("MIN(3.78787,3.78786)").eval().toPlainString()); - assertEquals("3.78786", new Expression("Min(3.78786,3.78787)").eval().toPlainString()); - assertEquals("2.123", new Expression("aBs(-2.123)").eval().toPlainString()); - assertEquals("2.123", new Expression("abs(2.123)").eval().toPlainString()); - } - - @Test - public void testRounding() { - assertEquals("3.8", new Expression("round(3.78787,1)").eval().toPlainString()); - assertEquals("3.788", new Expression("round(3.78787,3)").eval().toPlainString()); - assertEquals("3.734", new Expression("round(3.7345,3)").eval().toPlainString()); - assertEquals("-3.734", new Expression("round(-3.7345,3)").eval().toPlainString()); - assertEquals("-3.79", new Expression("round(-3.78787,2)").eval().toPlainString()); - assertEquals("123.79", new Expression("round(123.78787,2)").eval().toPlainString()); - assertEquals("3", new Expression("floor(3.78787)").eval().toPlainString()); - assertEquals("4", new Expression("ceiling(3.78787)").eval().toPlainString()); - assertEquals("-3", new Expression("floor(-2.1)").eval().toPlainString()); - assertEquals("-2", new Expression("ceiling(-2.1)").eval().toPlainString()); - } - - @Test - public void testMathContext() { - Expression e = new Expression("2.5/3").setPrecision(2); - assertEquals("0.83", e.eval().toPlainString()); - - e = new Expression("2.5/3").setPrecision(3); - assertEquals("0.833", e.eval().toPlainString()); - - e = new Expression("2.5/3").setPrecision(8); - assertEquals("0.83333333", e.eval().toPlainString()); - - e = new Expression("2.5/3").setRoundingMode(RoundingMode.DOWN); - assertEquals("0.8333333", e.eval().toPlainString()); - - e = new Expression("2.5/3").setRoundingMode(RoundingMode.UP); - assertEquals("0.8333334", e.eval().toPlainString()); - } - - @Test - public void unknownFunctionsFailGracefully() { - String err = ""; - try { - new Expression("unk(1,2,3)").eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - - assertEquals("Unknown function unk at character position 1", err); - } - - @Test - public void unknownOperatorsFailGracefully() { - String err = ""; - try { - new Expression("a |*| b").eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - - assertEquals("Unknown operator |*| at character position 3", err); - } - - @Test - public void testNull() { - Expression e = new Expression("null"); - assertNull(e.eval()); - } - - @Test - public void testCalculationWithNull() { - String err = ""; - try { - new Expression("null+1").eval(); - } catch (ArithmeticException e) { - err = e.getMessage(); - } - assertEquals("First operand may not be null", err); - - err = ""; - try { - new Expression("1 + NULL").eval(); - } catch (ArithmeticException e) { - err = e.getMessage(); - } - assertEquals("Second operand may not be null", err); - - err = ""; - try { - new Expression("round(Null, 1)").eval(); - } catch (ArithmeticException e) { - err = e.getMessage(); - } - assertEquals("First operand may not be null", err); - - err = ""; - try { - new Expression("round(1, NulL)").eval(); - } catch (ArithmeticException e) { - err = e.getMessage(); - } - assertEquals("Second operand may not be null", err); - } - - @Test - public void canEvalHexExpression() { - BigDecimal result = new Expression("0xcafe").eval(); - assertEquals("51966", result.toPlainString()); - } - - @Test - public void hexExpressionCanUseUpperCaseCharacters() { - BigDecimal result = new Expression("0XCAFE").eval(); - assertEquals("51966", result.toPlainString()); - } - - @Test - public void hexMinus() { - BigDecimal result = new Expression("-0XCAFE").eval(); - assertEquals("-51966", result.toPlainString()); - } - - @Test - public void hexMinusBlanks() { - BigDecimal result = new Expression(" 0xa + -0XCAFE ").eval(); - assertEquals("-51956", result.toPlainString()); - } - - @Test - public void longHexExpressionWorks() { - BigDecimal result = new Expression("0xcafebabe", MathContext.DECIMAL128).eval(); - assertEquals("3405691582", result.toPlainString()); - } - - @Test(expected = ExpressionException.class) - public void hexExpressionDoesNotAllowNonHexCharacters() { - BigDecimal result = new Expression("0xbaby").eval(); - } - - @Test(expected = NumberFormatException.class) - public void throwsExceptionIfDoesNotContainHexDigits() { - BigDecimal result = new Expression("0x").eval(); - } - - @Test - public void hexExpressionsEvaluatedAsExpected() { - BigDecimal result = new Expression("0xcafe + 0xbabe").eval(); - assertEquals("99772", result.toPlainString()); - } - - @Test - public void testResultZeroStripping() { - Expression expression = new Expression("200.40000 / 2"); - assertEquals("100.2", expression.eval().toPlainString()); - assertEquals("100.2000", expression.eval(false).toPlainString()); - } - - @Test - public void testImplicitMultiplication() { - Expression expression = new Expression("22(3+1)"); - assertEquals("88", expression.eval().toPlainString()); - - expression = new Expression("(a+b)(a-b)"); - assertEquals("-3", expression.with("a", "1").and("b", "2").eval().toPlainString()); - - expression = new Expression("0xA(a+b)"); - assertEquals("30", expression.with("a", "1").and("b", "2").eval().toPlainString()); - } - - @Test - public void testNoLeadingZero() { - Expression e = new Expression("0.1 + .1"); - assertEquals("0.2", e.eval().toPlainString()); - - e = new Expression(".2*.3"); - assertEquals("0.06", e.eval().toPlainString()); - - e = new Expression(".2*.3+.1"); - assertEquals("0.16", e.eval().toPlainString()); - } - - @Test - public void testUnexpectedComma() { - String err = ""; - try { - Expression expression = new Expression("2+3,8"); - expression.eval(); - } catch (ExpressionException e) { - err = e.getMessage(); - } - assertEquals("Unexpected comma at character position 3", err); - } -} - diff --git a/src/test/java/com/udojava/evalex/TestExposedComponents.java b/src/test/java/com/udojava/evalex/TestExposedComponents.java deleted file mode 100644 index 91d25a4..0000000 --- a/src/test/java/com/udojava/evalex/TestExposedComponents.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.udojava.evalex; - -import java.math.BigDecimal; -import java.util.List; -import org.junit.Assert; -import org.junit.Test; - -/** - * Created by fobbyal on 2/28/2016 at 3:48 PM. Project EvalEx - */ -public class TestExposedComponents { - - @Test - public void testDeclaredOperators() { - Expression expression = new Expression("c+d"); - int originalOperator = expression.getDeclaredOperators().size(); - expression.addOperator(expression.new Operator("$$", -1, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - return null; - } - }); - Assert.assertTrue("Operator List should have the new $$ operator", - expression.getDeclaredOperators().contains("$$")); - Assert.assertEquals("Should have an extra operators", expression.getDeclaredOperators().size(), - originalOperator + 1); - } - - @Test - public void testDeclaredVariables() { - Expression expression = new Expression("c+d"); - int originalVarCounts = expression.getDeclaredVariables().size(); - expression.setVariable("var1", new BigDecimal(12)); - - Assert.assertTrue("Variable list should have new var1 variable declared", - expression.getDeclaredVariables().contains("var1")); - Assert.assertEquals("Variable list should have q more declared variable", - expression.getDeclaredVariables().size(), originalVarCounts + 1); - } - - @Test - public void testDeclaredFunctionGetter() { - Expression expression = new Expression("a+b"); - int originalFunctionCount = expression.getDeclaredFunctions().size(); - expression.addFunction(expression.new Function("func1", 3) { - @Override - public BigDecimal eval(List parameters) { - return null; - } - }); - - Assert.assertTrue("Function list should have new func1 function declared", - expression.getDeclaredFunctions().contains("FUNC1")); - Assert.assertEquals("Function list should have one more function declared", - expression.getDeclaredFunctions().size(), originalFunctionCount + 1); - } - - - @Test(expected = RuntimeException.class) - public void testUnmodifiableFunctionList() { - Expression expression = new Expression("c+d"); - expression.getDeclaredFunctions().add("$$$"); - } - - @Test(expected = RuntimeException.class) - public void testUnmodifiableOperatorList() { - Expression expression = new Expression("c+d"); - expression.getDeclaredOperators().add("$$$"); - } - - @Test(expected = RuntimeException.class) - public void testUnmodifiableVariableList() { - Expression expression = new Expression("c+d"); - expression.getDeclaredVariables().add("$$$"); - } -} diff --git a/src/test/java/com/udojava/evalex/TestFactorial.java b/src/test/java/com/udojava/evalex/TestFactorial.java deleted file mode 100644 index ae0f68c..0000000 --- a/src/test/java/com/udojava/evalex/TestFactorial.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.udojava.evalex; - -import static org.junit.Assert.assertEquals; - -import org.junit.Test; - -public class TestFactorial { - - - @Test - public void testFactorial() { - - assertEquals("1", new Expression("FACT(0)").eval().toPlainString()); - assertEquals("1", new Expression("FACT(1)").eval().toPlainString()); - assertEquals("2", new Expression("FACT(2)").eval().toPlainString()); - assertEquals("6", new Expression("FACT(3)").eval().toPlainString()); - assertEquals("24", new Expression("FACT(4)").eval().toPlainString()); - - } - -} diff --git a/src/test/java/com/udojava/evalex/TestGetUsedVariables.java b/src/test/java/com/udojava/evalex/TestGetUsedVariables.java deleted file mode 100644 index e70926c..0000000 --- a/src/test/java/com/udojava/evalex/TestGetUsedVariables.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.udojava.evalex; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import java.math.BigDecimal; -import java.util.Collections; -import java.util.List; -import org.junit.Test; - -public class TestGetUsedVariables { - - @Test - public void testVars() { - Expression ex = new Expression("a/2*PI+MIN(e,b)"); - List usedVars = ex.getUsedVariables(); - assertEquals(2, usedVars.size()); - assertTrue(usedVars.contains("a")); - assertTrue(usedVars.contains("b")); - } - - @Test - public void testVarsLongNames() { - Expression ex = new Expression("var1/2*PI+MIN(var2,var3)"); - List usedVars = ex.getUsedVariables(); - assertEquals(3, usedVars.size()); - assertTrue(usedVars.contains("var1")); - assertTrue(usedVars.contains("var2")); - assertTrue(usedVars.contains("var3")); - } - - @Test - public void testVarsNothing() { - Expression ex = new Expression("1/2"); - List usedVars = ex.getUsedVariables(); - assertEquals(0, usedVars.size()); - } - - @Test - public void testStringComparison() { - // test for issue #267 (getUsedVariables() throws NPE in expressions containing string literals) - Expression ex = new Expression("foo=\"bar\"").setVariable("foo", (BigDecimal) null); - assertEquals(Collections.singletonList("foo"), ex.getUsedVariables()); - } -} diff --git a/src/test/java/com/udojava/evalex/TestLazyIf.java b/src/test/java/com/udojava/evalex/TestLazyIf.java deleted file mode 100644 index bc8fb34..0000000 --- a/src/test/java/com/udojava/evalex/TestLazyIf.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.udojava.evalex; - -import static org.junit.Assert.assertEquals; - -import java.math.BigDecimal; -import org.junit.Assert; -import org.junit.Test; - -/** - * Created by showdown on 4/5/2016 at 12:19 AM. Project EvalEx - */ -public class TestLazyIf { - - @Test - public void testLazyIf() { - Expression expression = new Expression("if(a=0,0,12/a)"); - expression.setVariable("a", new BigDecimal(0)); - Assert.assertEquals(expression.eval(), new BigDecimal(0)); - } - - @Test - public void testLazyIfWithNestedFunction() { - Expression expression = new Expression("if(a=0,0,abs(12/a))"); - expression.setVariable("a", new BigDecimal(0)); - Assert.assertEquals(expression.eval(), new BigDecimal(0)); - } - - @Test - public void testLazyIfWithNestedSuccessIf() { - Expression expression = new Expression("if(a=0,0,if(5/a>3,2,4))"); - expression.setVariable("a", new BigDecimal(0)); - Assert.assertEquals(expression.eval(), new BigDecimal(0)); - } - - @Test(expected = ArithmeticException.class) - public void testLazyIfWithNestedFailingIf() { - Expression expression = new Expression("if(a=0,if(5/a>3,2,4),0)"); - expression.setVariable("a", new BigDecimal(0)); - Assert.assertEquals(expression.eval(), new BigDecimal(0)); - } - - - @Test - public void testLazyIfWithNull() { - String err = ""; - try { - new Expression("if(a,0,12/a)").setVariable("a", "null").eval(); - } catch (ArithmeticException e) { - err = e.getMessage(); - } - assertEquals("Operand may not be null", err); - } - -} diff --git a/src/test/java/com/udojava/evalex/TestNested.java b/src/test/java/com/udojava/evalex/TestNested.java deleted file mode 100644 index bfa975a..0000000 --- a/src/test/java/com/udojava/evalex/TestNested.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.udojava.evalex; - -import static org.junit.Assert.assertEquals; - -import java.math.BigDecimal; -import java.math.MathContext; -import java.math.RoundingMode; -import java.util.List; -import org.junit.Test; - -public class TestNested { - - @Test - public void testNestedVars() { - String x = "1"; - String y = "2"; - String z = "2*x + 3*y"; - String a = "2*x + 4*z"; - - Expression e = new Expression(a); - e.with("x", x); - e.with("y", y); - e.with("z", z); - - assertEquals("34", e.eval().toString()); - } - - @Test - public void testReplacements() { - Expression e = new Expression("3+a+aa+aaa").with("a", "1*x") - .with("aa", "2*x").with("aaa", "3*x").with("x", "2"); - assertEquals("15", e.eval().toString()); - } - - @Test - public void testNestedOutOfOrderVars() { - String x = "7"; - String y = "5"; - String z = "a+b"; - String a = "p+q"; - String p = "b+q"; - String q = "x+y"; - String b = "x*2+y"; - Expression e = new Expression(z); - e.with("q", q); - e.with("p", p); - e.with("a", a); - e.with("b", b); - e.with("x", x); - e.with("y", y); - // a+b = p+q+b = b+q+q+b = 2b+2q = 2(x*2 + y) + 2(x+y) = 2(7*2+5) + 2(7+5) = - String res = e.eval().toString(); - assertEquals("62", res); - } - - @Test - public void testNestedOutOfOrderVarsWithFunc() { - String x = "7"; - String y = "5"; - String z = "a+b"; - String a = "p+q"; - String p = "b+q"; - String q = "myAvg(x,b)"; - String b = "x*2+y"; - Expression e = new Expression(z); - e.with("q", q); - e.with("p", p); - e.with("a", a); - e.with("b", b); - e.with("x", x); - e.with("y", y); - e.addFunction(e.new Function("myAvg", 2) { - - @Override - public BigDecimal eval(List parameters) { - assertEquals(2, parameters.size()); - BigDecimal two = new BigDecimal(2); - return (parameters.get(0).add(parameters.get(1))).divide(two, MathContext.DECIMAL32); - } - }); - // a+b = p+q+b = b+q+q+b = 2b+2q = 2(x*2 + y) + 2(myAvg(x+b)) = 2(7*2+5) + 2((7+(7*2+5))/2 = 38 + 26 = 64 - String res = e.eval().toPlainString(); - assertEquals("64", res); - } - - @Test - public void testNestedOutOfOrderVarsWithOperators() { - MathContext mc = new MathContext(12, RoundingMode.HALF_UP); - Expression e = new Expression("a+y", mc); - e.addOperator(e.new Operator(">>", 30, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - return v1.movePointRight(v2.toBigInteger().intValue()); - } - }); - e.with("b", "123.45678"); // e=((123.45678 >> 2))+2+y - e.with("x", "b >> 2"); // e=((b >> 2)+2)+y - e.with("a", "X+2"); // e=(x+2)+y - e.with("y", "5"); // e=((123.45678>>2)+2)+5 = 12345.678+2+5 = 12352.678 - BigDecimal res = e.eval(); - String resStr = res.toPlainString(); - assertEquals("12352.678", resStr); - } -} diff --git a/src/test/java/com/udojava/evalex/TestRPN.java b/src/test/java/com/udojava/evalex/TestRPN.java deleted file mode 100644 index c007f79..0000000 --- a/src/test/java/com/udojava/evalex/TestRPN.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.udojava.evalex; - -import static org.junit.Assert.assertEquals; - -import org.junit.Test; - - -public class TestRPN { - - @Test - public void testSimple() { - - assertEquals("1 2 +", new Expression("1+2").toRPN()); - assertEquals("1 2 4 / +", new Expression("1+2/4").toRPN()); - assertEquals("1 2 + 4 /", new Expression("(1+2)/4").toRPN()); - assertEquals("1.9 2.8 + 4.7 /", new Expression("(1.9+2.8)/4.7").toRPN()); - assertEquals("1.98 2.87 + 4.76 /", new Expression("(1.98+2.87)/4.76").toRPN()); - assertEquals("3 4 2 * 1 5 - 2 3 ^ ^ / +", - new Expression("3 + 4 * 2 / ( 1 - 5 ) ^ 2 ^ 3").toRPN()); - } - - @Test - public void testFunctions() { - - assertEquals("( 23.6 SIN", new Expression("SIN(23.6)").toRPN()); - assertEquals("( 7 -u 8 MAX", new Expression("MAX(-7,8)").toRPN()); - assertEquals("( ( 3.7 SIN ( 2.6 8.0 -u MAX MAX", - new Expression("MAX(SIN(3.7),MAX(2.6,-8.0))").toRPN()); - } - - @Test - public void testOperatorsInFunctions() { - - assertEquals("( 23.6 4 / SIN", new Expression("SIN(23.6/4)").toRPN()); - } - - @Test - public void testNested() { - Expression e = new Expression("x+y"); - e.with("x", "a+b"); - assertEquals("a b + y +", e.toRPN()); - } - - @Test - public void testComplexNested() { - Expression e = new Expression("x+y"); - e.with("a", "p/q"); - e.with("p", "q*y"); - e.with("x", "p*12"); // x y + = p 12 * y + - e.with("y", "a"); // p 12 * p q / + - e.with("p", "15"); - e.with("q", "14"); - String rpn = e.toRPN(); - assertEquals("p 12 * p q / +", rpn); - - } -} diff --git a/src/test/java/com/udojava/evalex/TestSciNotation.java b/src/test/java/com/udojava/evalex/TestSciNotation.java deleted file mode 100644 index 499e42c..0000000 --- a/src/test/java/com/udojava/evalex/TestSciNotation.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.udojava.evalex; - -import static org.junit.Assert.assertEquals; - -import java.math.MathContext; -import org.junit.Test; - -public class TestSciNotation { - - @Test - public void testSimple() { - Expression e = new Expression("1e10"); - assertEquals("10000000000", e.eval().toPlainString()); - - e = new Expression("1E10"); - assertEquals("10000000000", e.eval().toPlainString()); - - e = new Expression("123.456E3"); - assertEquals("123456", e.eval().toPlainString()); - - e = new Expression("2.5e0"); - assertEquals("2.5", e.eval().toPlainString()); - } - - @Test - public void testNegative() { - Expression e = new Expression("1e-10"); - assertEquals("0.0000000001", e.eval().toPlainString()); - - e = new Expression("1E-10"); - assertEquals("0.0000000001", e.eval().toPlainString()); - - e = new Expression("2135E-4"); - assertEquals("0.2135", e.eval().toPlainString()); - } - - @Test //@Ignore("Expected Failures: not implemented yet") - public void testPositive() { - Expression e = new Expression("1e+10"); - assertEquals("10000000000", e.eval().toPlainString()); - - e = new Expression("1E+10"); - assertEquals("10000000000", e.eval().toPlainString()); - } - - @Test - public void testCombined() { - Expression e = new Expression("sqrt(152.399025e6)", MathContext.DECIMAL64); - assertEquals("12345", e.eval().toPlainString()); - - e = new Expression("sin(3.e1)"); - assertEquals("0.5", e.eval().toPlainString()); - - e = new Expression("sin( 3.e1)"); - assertEquals("0.5", e.eval().toPlainString()); - - e = new Expression("sin(3.e1 )"); - assertEquals("0.5", e.eval().toPlainString()); - - e = new Expression("sin( 3.e1 )"); - assertEquals("0.5", e.eval().toPlainString()); - - e = new Expression("2.2e-16 * 10.2"); - - assertEquals("2.244E-15", e.eval().toString()); - } - - @Test(expected = NumberFormatException.class) - public void testError1() { - Expression e = new Expression("1234e-2.3"); - e.eval(); - } - - @Test(expected = NumberFormatException.class) - public void testError2() { - Expression e = new Expression("1234e2.3"); - e.eval(); - } - - @Test - public void testError3() { - String err = ""; - Expression e = new Expression("e2"); - try { - e.eval(); - } catch (RuntimeException ex) { - err = ex.getMessage(); - } - assertEquals("Unknown operator or function: e2", err); - } -} diff --git a/src/test/java/com/udojava/evalex/TestSettings.java b/src/test/java/com/udojava/evalex/TestSettings.java deleted file mode 100644 index 8995b95..0000000 --- a/src/test/java/com/udojava/evalex/TestSettings.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.udojava.evalex; - -import static org.junit.Assert.assertEquals; - -import java.math.MathContext; -import org.junit.Test; - -public class TestSettings { - - @Test - public void testDefaults() { - ExpressionSettings settings = ExpressionSettings.builder().build(); - assertEquals(MathContext.DECIMAL32, settings.getMathContext()); - assertEquals(Expression.OPERATOR_PRECEDENCE_POWER, settings.getPowerOperatorPrecedence()); - } - - @Test - public void testSet() { - ExpressionSettings settings = ExpressionSettings.builder() - .mathContext(MathContext.DECIMAL128) - .powerOperatorPrecedence(99) - .build(); - assertEquals(MathContext.DECIMAL128, settings.getMathContext()); - assertEquals(99, settings.getPowerOperatorPrecedence()); - } - - @Test - public void testSetPowerHigher() { - ExpressionSettings settings = ExpressionSettings.builder() - .powerOperatorPrecedenceHigher() - .build(); - assertEquals(Expression.OPERATOR_PRECEDENCE_POWER_HIGHER, - settings.getPowerOperatorPrecedence()); - } - - @Test - public void testDefaultPowerPrecedence() { - assertEquals("4", new Expression("-2^2").eval().toString()); - } - - @Test - public void testHigherPowerPrecedence() { - assertEquals("-4", new Expression("-2^2", ExpressionSettings.builder() - .powerOperatorPrecedenceHigher() - .build()).eval().toString()); - } -} diff --git a/src/test/java/com/udojava/evalex/TestTokenizer.java b/src/test/java/com/udojava/evalex/TestTokenizer.java deleted file mode 100644 index 7184ac3..0000000 --- a/src/test/java/com/udojava/evalex/TestTokenizer.java +++ /dev/null @@ -1,391 +0,0 @@ -package com.udojava.evalex; - -import com.udojava.evalex.Expression.Token; -import com.udojava.evalex.Expression.TokenType; -import java.math.BigDecimal; -import java.util.Iterator; - -import org.junit.Test; -import org.junit.function.ThrowingRunnable; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - - -public class TestTokenizer { - - private void assertToken(String surface, Expression.TokenType type, Expression.Token actual) { - assertEquals(surface, actual.surface); - assertEquals(type, actual.type); - } - - @Test - public void testSpacesFunctions() { - Expression e; - Iterator i; - - e = new Expression("sin (30)"); - i = e.getExpressionTokenizer(); - assertToken("sin", TokenType.FUNCTION, i.next()); - assertToken("(", TokenType.OPEN_PAREN, i.next()); - assertToken("30", TokenType.LITERAL, i.next()); - assertToken(")", TokenType.CLOSE_PAREN, i.next()); - assertFalse(i.hasNext()); - } - - @Test - public void testSpacesFunctionsVariablesOperators() { - Expression e; - Iterator i; - - e = new Expression(" sin ( 30 + x ) "); - i = e.getExpressionTokenizer(); - assertToken("sin", TokenType.FUNCTION, i.next()); - assertToken("(", TokenType.OPEN_PAREN, i.next()); - assertToken("30", TokenType.LITERAL, i.next()); - assertToken("+", TokenType.OPERATOR, i.next()); - assertToken("x", TokenType.VARIABLE, i.next()); - assertToken(")", TokenType.CLOSE_PAREN, i.next()); - assertFalse(i.hasNext()); - } - - @Test - public void testNumbers() { - Expression e; - Iterator i; - - e = new Expression("1"); - i = e.getExpressionTokenizer(); - assertToken("1", TokenType.LITERAL, i.next()); - assertFalse(i.hasNext()); - assertNull(i.next()); - - e = new Expression("-1"); - i = e.getExpressionTokenizer(); - assertToken("-u", TokenType.UNARY_OPERATOR, i.next()); - assertToken("1", TokenType.LITERAL, i.next()); - assertFalse(i.hasNext()); - assertNull(i.next()); - - e = new Expression("123"); - i = e.getExpressionTokenizer(); - assertToken("123", TokenType.LITERAL, i.next()); - assertFalse(i.hasNext()); - assertNull(i.next()); - - e = new Expression("-123"); - i = e.getExpressionTokenizer(); - assertToken("-u", TokenType.UNARY_OPERATOR, i.next()); - assertToken("123", TokenType.LITERAL, i.next()); - assertFalse(i.hasNext()); - assertNull(i.next()); - - e = new Expression("123.4"); - i = e.getExpressionTokenizer(); - assertToken("123.4", TokenType.LITERAL, i.next()); - assertFalse(i.hasNext()); - assertNull(i.next()); - - e = new Expression("-123.456"); - i = e.getExpressionTokenizer(); - assertToken("-u", TokenType.UNARY_OPERATOR, i.next()); - assertToken("123.456", TokenType.LITERAL, i.next()); - assertFalse(i.hasNext()); - assertNull(i.next()); - - e = new Expression(".1"); - i = e.getExpressionTokenizer(); - assertToken(".1", TokenType.LITERAL, i.next()); - assertFalse(i.hasNext()); - assertNull(i.next()); - - e = new Expression("-.1"); - i = e.getExpressionTokenizer(); - assertToken("-u", TokenType.UNARY_OPERATOR, i.next()); - assertToken(".1", TokenType.LITERAL, i.next()); - assertFalse(i.hasNext()); - assertNull(i.next()); - } - - @Test - public void testTokenizerExtraSpaces() { - Expression e = new Expression("1 "); - Iterator i = e.getExpressionTokenizer(); - assertTrue(i.hasNext()); - assertToken("1", TokenType.LITERAL, i.next()); - assertFalse(i.hasNext()); - assertNull(i.next()); - - e = new Expression(" "); - i = e.getExpressionTokenizer(); - assertFalse(i.hasNext()); - assertNull(i.next()); - - e = new Expression(" 1 "); - i = e.getExpressionTokenizer(); - assertTrue(i.hasNext()); - assertToken("1", TokenType.LITERAL, i.next()); - assertFalse(i.hasNext()); - assertNull(i.next()); - - e = new Expression(" 1 + 2 "); - i = e.getExpressionTokenizer(); - assertToken("1", TokenType.LITERAL, i.next()); - assertToken("+", TokenType.OPERATOR, i.next()); - assertToken("2", TokenType.LITERAL, i.next()); - assertFalse(i.hasNext()); - assertNull(i.next()); - } - - @Test - public void testTokenizer1() { - Expression e = new Expression("1+2"); - Iterator i = e.getExpressionTokenizer(); - - assertToken("1", TokenType.LITERAL, i.next()); - assertToken("+", TokenType.OPERATOR, i.next()); - assertToken("2", TokenType.LITERAL, i.next()); - } - - @Test - public void testTokenizer2() { - Expression e = new Expression("1 + 2"); - Iterator i = e.getExpressionTokenizer(); - - assertToken("1", TokenType.LITERAL, i.next()); - assertToken("+", TokenType.OPERATOR, i.next()); - assertToken("2", TokenType.LITERAL, i.next()); - } - - @Test - public void testTokenizer3() { - Expression e = new Expression(" 1 + 2 "); - Iterator i = e.getExpressionTokenizer(); - - assertToken("1", TokenType.LITERAL, i.next()); - assertToken("+", TokenType.OPERATOR, i.next()); - assertToken("2", TokenType.LITERAL, i.next()); - } - - @Test - public void testTokenizer4() { - Expression e = new Expression("1+2-3/4*5"); - Iterator i = e.getExpressionTokenizer(); - - assertToken("1", TokenType.LITERAL, i.next()); - assertToken("+", TokenType.OPERATOR, i.next()); - assertToken("2", TokenType.LITERAL, i.next()); - assertToken("-", TokenType.OPERATOR, i.next()); - assertToken("3", TokenType.LITERAL, i.next()); - assertToken("/", TokenType.OPERATOR, i.next()); - assertToken("4", TokenType.LITERAL, i.next()); - assertToken("*", TokenType.OPERATOR, i.next()); - assertToken("5", TokenType.LITERAL, i.next()); - } - - @Test - public void testTokenizer5() { - Expression e = new Expression("1+2.1-3.45/4.982*5.0"); - Iterator i = e.getExpressionTokenizer(); - - assertToken("1", TokenType.LITERAL, i.next()); - assertToken("+", TokenType.OPERATOR, i.next()); - assertToken("2.1", TokenType.LITERAL, i.next()); - assertToken("-", TokenType.OPERATOR, i.next()); - assertToken("3.45", TokenType.LITERAL, i.next()); - assertToken("/", TokenType.OPERATOR, i.next()); - assertToken("4.982", TokenType.LITERAL, i.next()); - assertToken("*", TokenType.OPERATOR, i.next()); - assertToken("5.0", TokenType.LITERAL, i.next()); - - } - - @Test - public void testTokenizer6() { - Expression e = new Expression("-3+4*-1"); - Iterator i = e.getExpressionTokenizer(); - - assertToken("-u", TokenType.UNARY_OPERATOR, i.next()); - assertToken("3", TokenType.LITERAL, i.next()); - assertToken("+", TokenType.OPERATOR, i.next()); - assertToken("4", TokenType.LITERAL, i.next()); - assertToken("*", TokenType.OPERATOR, i.next()); - assertToken("-u", TokenType.UNARY_OPERATOR, i.next()); - assertToken("1", TokenType.LITERAL, i.next()); - } - - @Test - public void testTokenizer7() { - Expression e = new Expression("(-3+4)*-1/(7-(5*-8))"); - Iterator i = e.getExpressionTokenizer(); - - assertToken("(", TokenType.OPEN_PAREN, i.next()); - assertToken("-u", TokenType.UNARY_OPERATOR, i.next()); - assertToken("3", TokenType.LITERAL, i.next()); - assertToken("+", TokenType.OPERATOR, i.next()); - assertToken("4", TokenType.LITERAL, i.next()); - assertToken(")", TokenType.CLOSE_PAREN, i.next()); - - assertToken("*", TokenType.OPERATOR, i.next()); - assertToken("-u", TokenType.UNARY_OPERATOR, i.next()); - assertToken("1", TokenType.LITERAL, i.next()); - assertToken("/", TokenType.OPERATOR, i.next()); - assertToken("(", TokenType.OPEN_PAREN, i.next()); - assertToken("7", TokenType.LITERAL, i.next()); - assertToken("-", TokenType.OPERATOR, i.next()); - assertToken("(", TokenType.OPEN_PAREN, i.next()); - assertToken("5", TokenType.LITERAL, i.next()); - assertToken("*", TokenType.OPERATOR, i.next()); - assertToken("-u", TokenType.UNARY_OPERATOR, i.next()); - assertToken("8", TokenType.LITERAL, i.next()); - assertToken(")", TokenType.CLOSE_PAREN, i.next()); - assertToken(")", TokenType.CLOSE_PAREN, i.next()); - } - - @Test - public void testTokenizer8() { - Expression e = new Expression("(1.9+2.8)/4.7"); - Iterator i = e.getExpressionTokenizer(); - - assertToken("(", TokenType.OPEN_PAREN, i.next()); - assertToken("1.9", TokenType.LITERAL, i.next()); - assertToken("+", TokenType.OPERATOR, i.next()); - assertToken("2.8", TokenType.LITERAL, i.next()); - assertToken(")", TokenType.CLOSE_PAREN, i.next()); - assertToken("/", TokenType.OPERATOR, i.next()); - assertToken("4.7", TokenType.LITERAL, i.next()); - - } - - @Test - public void testTokenizerFunction1() { - Expression e = new Expression("ABS(3.5)"); - Iterator i = e.getExpressionTokenizer(); - - assertToken("ABS", TokenType.FUNCTION, i.next()); - assertToken("(", TokenType.OPEN_PAREN, i.next()); - assertToken("3.5", TokenType.LITERAL, i.next()); - assertToken(")", TokenType.CLOSE_PAREN, i.next()); - } - - @Test - public void testTokenizerFunction2() { - Expression e = new Expression("3-ABS(3.5)/9"); - Iterator i = e.getExpressionTokenizer(); - - assertToken("3", TokenType.LITERAL, i.next()); - assertToken("-", TokenType.OPERATOR, i.next()); - assertToken("ABS", TokenType.FUNCTION, i.next()); - assertToken("(", TokenType.OPEN_PAREN, i.next()); - assertToken("3.5", TokenType.LITERAL, i.next()); - assertToken(")", TokenType.CLOSE_PAREN, i.next()); - assertToken("/", TokenType.OPERATOR, i.next()); - assertToken("9", TokenType.LITERAL, i.next()); - - } - - @Test - - public void testTokenizerFunction3() { - Expression e = new Expression("MAX(3.5,5.2)"); - Iterator i = e.getExpressionTokenizer(); - - assertToken("MAX", TokenType.FUNCTION, i.next()); - assertToken("(", TokenType.OPEN_PAREN, i.next()); - assertToken("3.5", TokenType.LITERAL, i.next()); - assertToken(",", TokenType.COMMA, i.next()); - assertToken("5.2", TokenType.LITERAL, i.next()); - assertToken(")", TokenType.CLOSE_PAREN, i.next()); - } - - @Test - public void testTokenizerFunction4() { - Expression e = new Expression("3-MAX(3.5,5.2)/9"); - Iterator i = e.getExpressionTokenizer(); - - assertToken("3", TokenType.LITERAL, i.next()); - assertToken("-", TokenType.OPERATOR, i.next()); - assertToken("MAX", TokenType.FUNCTION, i.next()); - assertToken("(", TokenType.OPEN_PAREN, i.next()); - assertToken("3.5", TokenType.LITERAL, i.next()); - assertToken(",", TokenType.COMMA, i.next()); - assertToken("5.2", TokenType.LITERAL, i.next()); - assertToken(")", TokenType.CLOSE_PAREN, i.next()); - assertToken("/", TokenType.OPERATOR, i.next()); - assertToken("9", TokenType.LITERAL, i.next()); - } - - @Test - public void testTokenizerFunction5() { - Expression e = new Expression("3/MAX(-3.5,-5.2)/9"); - Iterator i = e.getExpressionTokenizer(); - - assertToken("3", TokenType.LITERAL, i.next()); - assertToken("/", TokenType.OPERATOR, i.next()); - assertToken("MAX", TokenType.FUNCTION, i.next()); - assertToken("(", TokenType.OPEN_PAREN, i.next()); - assertToken("-u", TokenType.UNARY_OPERATOR, i.next()); - assertToken("3.5", TokenType.LITERAL, i.next()); - assertToken(",", TokenType.COMMA, i.next()); - assertToken("-u", TokenType.UNARY_OPERATOR, i.next()); - assertToken("5.2", TokenType.LITERAL, i.next()); - assertToken(")", TokenType.CLOSE_PAREN, i.next()); - assertToken("/", TokenType.OPERATOR, i.next()); - assertToken("9", TokenType.LITERAL, i.next()); - } - - @Test - public void testInsertImplicitMultiplication() { - Expression e = new Expression("22(3+1)"); - Iterator i = e.getExpressionTokenizer(); - - assertToken("22", TokenType.LITERAL, i.next()); - assertToken("(", TokenType.OPEN_PAREN, i.next()); - assertToken("3", TokenType.LITERAL, i.next()); - assertToken("+", TokenType.OPERATOR, i.next()); - assertToken("1", TokenType.LITERAL, i.next()); - assertToken(")", TokenType.CLOSE_PAREN, i.next()); - } - - @Test - public void testBracesCustomOperatorAndInIf() { - Expression e = new Expression("if( (a=0) and (b=0), 0, 1)"); - e.addOperator(new AbstractOperator("AND", Expression.OPERATOR_PRECEDENCE_AND, false, true) { - @Override - public BigDecimal eval(BigDecimal v1, BigDecimal v2) { - boolean b1 = v1.compareTo(BigDecimal.ZERO) != 0; - if (!b1) { - return BigDecimal.ZERO; - } - boolean b2 = v2.compareTo(BigDecimal.ZERO) != 0; - return b2 ? BigDecimal.ONE : BigDecimal.ZERO; - } - }); - - BigDecimal result = e.with("a", "0").and("b", "0").eval(); - assertEquals("0", result.toPlainString()); - } - - @Test - public void testStringLiterals() { - Iterator i = new Expression("\"foo\"").getExpressionTokenizer(); - assertEquals(TokenType.STRINGPARAM, i.next().type); - assertFalse(i.hasNext()); - } - - @Test - public void testUnterminatedStringLiterals() { - TokenizerException ex = assertThrows(TokenizerException.class, new ThrowingRunnable() { - @Override - public void run() { - new Expression("\"foo").getExpressionTokenizer().next(); - } - }); - - assertEquals("unterminated string literal at character position 0", ex.getMessage()); - } - -} diff --git a/src/test/java/com/udojava/evalex/TestVarArgs.java b/src/test/java/com/udojava/evalex/TestVarArgs.java deleted file mode 100644 index 5556ada..0000000 --- a/src/test/java/com/udojava/evalex/TestVarArgs.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.udojava.evalex; - -import static org.junit.Assert.assertEquals; - -import com.udojava.evalex.Expression.ExpressionException; -import java.math.BigDecimal; -import java.math.MathContext; -import java.util.List; -import org.junit.Test; - -public class TestVarArgs { - - @Test - public void testSimple() { - Expression e = new Expression("max(1)"); - assertEquals("1", e.eval().toPlainString()); - - e = new Expression("max(4,8)"); - assertEquals("8", e.eval().toPlainString()); - - e = new Expression("max(12,4,8)"); - assertEquals("12", e.eval().toPlainString()); - - e = new Expression("max(12,4,8,16,32)"); - assertEquals("32", e.eval().toPlainString()); - } - - @Test - public void testNested() { - Expression e = new Expression("max(1,2,max(3,4,5,max(9,10,3,4,5),8),7)"); - assertEquals("10", e.eval().toPlainString()); - } - - @Test - public void testZero() { - Expression e = new Expression("max(0)"); - assertEquals("0", e.eval().toPlainString()); - - e = new Expression("max(0,3)"); - assertEquals("3", e.eval().toPlainString()); - - e = new Expression("max(2,0,-3)"); - assertEquals("2", e.eval().toPlainString()); - - e = new Expression("max(-2,0,-3)"); - assertEquals("0", e.eval().toPlainString()); - - e = new Expression("max(0,0,0,0)"); - assertEquals("0", e.eval().toPlainString()); - } - - @Test - public void testError() { - String err = ""; - Expression e = new Expression("max()"); - try { - e.eval(); - } catch (ExpressionException ex) { - err = ex.getMessage(); - } - assertEquals("MAX requires at least one parameter", err); - } - - @Test - public void testCustomFunction1() { - Expression e = new Expression("3 * AVG(2,4)"); - e.addFunction(e.new Function("AVG", -1) { - @Override - public BigDecimal eval(List parameters) { - if (parameters.size() == 0) { - throw new ExpressionException("AVG requires at least one parameter"); - } - BigDecimal avg = new BigDecimal(0); - for (BigDecimal parameter : parameters) { - avg = avg.add(parameter); - } - return avg.divide(new BigDecimal(parameters.size()), MathContext.DECIMAL32); - } - }); - - assertEquals("9", e.eval().toPlainString()); - } - - @Test - public void testCustomFunction2() { - Expression e = new Expression("4 * AVG(2,4,6,8,10,12)"); - e.addFunction(e.new Function("AVG", -1) { - @Override - public BigDecimal eval(List parameters) { - if (parameters.size() == 0) { - throw new ExpressionException("AVG requires at least one parameter"); - } - BigDecimal avg = new BigDecimal(0); - for (BigDecimal parameter : parameters) { - avg = avg.add(parameter); - } - return avg.divide(new BigDecimal(parameters.size()), MathContext.DECIMAL32); - } - }); - - assertEquals("28", e.eval().toPlainString()); - } -} diff --git a/src/test/java/com/udojava/evalex/TestVariableCharacters.java b/src/test/java/com/udojava/evalex/TestVariableCharacters.java deleted file mode 100644 index 6d110e1..0000000 --- a/src/test/java/com/udojava/evalex/TestVariableCharacters.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.udojava.evalex; - -import static org.junit.Assert.assertEquals; - -import org.junit.Test; - -public class TestVariableCharacters { - - @Test - public void testBadVarChar() { - String err = ""; - try { - Expression expression = new Expression("a.b/2*PI+MIN(e,b)"); - expression.eval(); - } catch (Expression.ExpressionException e) { - err = e.getMessage(); - } - assertEquals("Unknown operator . at character position 2", err); - } - - @Test - public void testAddedVarChar() { - String err = ""; - Expression expression; - - try { - expression = new Expression("a.b/2*PI+MIN(e,b)").setVariableCharacters("_"); - expression.eval(); - } catch (Expression.ExpressionException e) { - err = e.getMessage(); - } - assertEquals("Unknown operator . at character position 2", err); - - try { - expression = new Expression("a.b/2*PI+MIN(e,b)").setVariableCharacters("_."); - expression.eval(); - } catch (Expression.ExpressionException e) { - err = e.getMessage(); - } - assertEquals("Unknown operator or function: a.b", err); - - expression = new Expression("a.b/2*PI+MIN(e,b)").setVariableCharacters("_."); - assertEquals("5.859875", expression.with("a.b", "2").and("b", "3").eval().toPlainString()); - - try { - expression = new Expression(".a.b/2*PI+MIN(e,b)").setVariableCharacters("_."); - expression.eval(); - } catch (Expression.ExpressionException e) { - err = e.getMessage(); - } - assertEquals("Unknown unary operator . at character position 1", err); - - expression = new Expression("a.b/2*PI+MIN(e,b)").setVariableCharacters("_.") - .setFirstVariableCharacters("."); - assertEquals("5.859875", expression.with("a.b", "2").and("b", "3").eval().toPlainString()); - } - - @Test - public void testFirstVarChar() { - Expression expression = new Expression("a.b*$PI").setVariableCharacters(".") - .setFirstVariableCharacters("$"); - assertEquals("6", expression.with("a.b", "2").and("$PI", "3").eval().toPlainString()); - - } -} diff --git a/src/test/java/com/udojava/evalex/TestVariables.java b/src/test/java/com/udojava/evalex/TestVariables.java deleted file mode 100644 index fb9ebe5..0000000 --- a/src/test/java/com/udojava/evalex/TestVariables.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.udojava.evalex; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -import java.math.BigDecimal; -import java.math.MathContext; -import org.junit.Test; - -public class TestVariables { - - @Test - public void testVars() { - assertEquals("3.141593", new Expression("PI").eval().toString()); - assertEquals( - "3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679", - new Expression("PI").setPrecision(MathContext.UNLIMITED.getPrecision()).eval().toString()); - assertEquals("3.141592653589793238462643383279503", - new Expression("PI").setPrecision(MathContext.DECIMAL128.getPrecision()).eval().toString()); - assertEquals("3.141592653589793", - new Expression("PI").setPrecision(MathContext.DECIMAL64.getPrecision()).eval().toString()); - assertEquals("3.141593", - new Expression("PI").setPrecision(MathContext.DECIMAL32.getPrecision()).eval().toString()); - assertEquals("6.283186", new Expression("PI*2.0").eval().toString()); - assertEquals("21", - new Expression("3*x").setVariable("x", new BigDecimal("7")) - .eval().toString()); - assertEquals( - "20", - new Expression("(a^2)+(b^2)") - .setVariable("a", new BigDecimal("2")) - .setVariable("b", new BigDecimal("4")).eval() - .toPlainString()); - assertEquals( - "68719480000", - new Expression("a^(2+b)^2") - .setVariable("a", "2") - .setVariable("b", "4").eval() - .toPlainString()); - } - - @Test - public void testSubstitution() { - Expression e = new Expression("x+y"); - - assertEquals("2", e.with("x", "1").and("y", "1").eval().toPlainString()); - assertEquals("1", e.with("y", "0").eval().toPlainString()); - assertEquals("0", e.with("x", "0").eval().toPlainString()); - } - - @Test - public void testWith() { - assertEquals("21", - new Expression("3*x").with("x", new BigDecimal("7")) - .eval().toString()); - assertEquals( - "20", - new Expression("(a^2)+(b^2)") - .with("a", new BigDecimal("2")) - .with("b", new BigDecimal("4")).eval() - .toPlainString()); - assertEquals( - "68719480000", - new Expression("a^(2+b)^2") - .with("a", "2") - .with("b", "4").eval() - .toPlainString()); - - assertEquals( - "68719480000", - new Expression("_a^(2+_b)^2") - .with("_a", "2") - .with("_b", "4").eval() - .toPlainString()); - } - - @Test - public void testNames() { - assertEquals("21", - new Expression("3*longname").with("longname", new BigDecimal("7")) - .eval().toString()); - - assertEquals("21", - new Expression("3*longname1").with("longname1", new BigDecimal("7")) - .eval().toString()); - - assertEquals("21", - new Expression("3*_longname1").with("_longname1", new BigDecimal("7")) - .eval().toString()); - } - - @Test(expected = Expression.ExpressionException.class) - public void failsIfVariableDoesNotExist() { - new Expression("3*unknown").eval(); - } - - @Test - public void testNullVariable() { - Expression e; - e = new Expression("a").with("a", "null"); - assertNull(e.eval()); - - e = new Expression("a").with("a", (BigDecimal) null); - assertNull(e.eval()); - - String err = ""; - try { - new Expression("a+1").with("a", "null").eval(); - } catch (ArithmeticException ex) { - err = ex.getMessage(); - } - assertEquals("First operand may not be null", err); - } -}