From 34e00da79510acc986934725e051c3337a1594f8 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 24 Sep 2020 13:32:23 -0700 Subject: [PATCH 001/289] Update Bazel CI workflow Build all Oppia targets in Bazel rather than just the binary target. --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 30a44cade99..aca1c0c2561 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -133,7 +133,7 @@ jobs: path: app/build/reports bazel_build_app: - name: Build Binary with Bazel + name: Build Oppia targets with Bazel runs-on: ${{ matrix.os }} strategy: matrix: @@ -150,19 +150,19 @@ jobs: uses: actions/setup-java@v1 with: java-version: 9 - - name: Extract Android Tools + - name: Extract Android tools run: | mkdir -p $GITHUB_WORKSPACE/tmp/android_tools cd $HOME/oppia-bazel unzip bazel-tools.zip tar -xf $HOME/oppia-bazel/android_tools.tar.gz -C $GITHUB_WORKSPACE/tmp/android_tools - - name: Unzip Bazel Binary and Build Oppia + - name: Unzip Bazel binary and build all Oppia targets run: | cd $HOME/oppia-bazel unzip bazel-build.zip cd $GITHUB_WORKSPACE chmod a+x $HOME/oppia-bazel/bazel - $HOME/oppia-bazel/bazel build //:oppia --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx + $HOME/oppia-bazel/bazel build //... --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx cp $GITHUB_WORKSPACE/bazel-bin/oppia.apk /home/runner/work/oppia-android/oppia-android/ - uses: actions/upload-artifact@v2 with: From 3f0cd724d77bf25bfe053e7d1e14ad9dfe67c419 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 24 Sep 2020 13:35:47 -0700 Subject: [PATCH 002/289] Update main.yml Reset the workflow name so that it doesn't need to be renamed in GitHub settings. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aca1c0c2561..f4440b323c0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -133,7 +133,7 @@ jobs: path: app/build/reports bazel_build_app: - name: Build Oppia targets with Bazel + name: Build Binary with Bazel runs-on: ${{ matrix.os }} strategy: matrix: From 5c92f2ccd9f5f3d14cd08c7cce160fb1d9b75cd9 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 13:56:59 -0800 Subject: [PATCH 003/289] Introduce remote caching in Bazel. This uses a remote storage service with a local file encrypted using git-secret to act as the authentication key for Bazel to read & write artifacts to the service for caching purposes. --- .github/workflows/main.yml | 22 ++++++++++++++++-- .gitignore | 3 +++ .gitsecret/keys/pubring.kbx | Bin 0 -> 3939 bytes .gitsecret/keys/pubring.kbx~ | Bin 0 -> 1980 bytes .gitsecret/keys/trustdb.gpg | Bin 0 -> 1200 bytes .gitsecret/paths/mapping.cfg | 1 + ...kflow-remote-cache-credentials.json.secret | Bin 0 -> 2543 bytes 7 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 .gitsecret/keys/pubring.kbx create mode 100644 .gitsecret/keys/pubring.kbx~ create mode 100644 .gitsecret/keys/trustdb.gpg create mode 100644 .gitsecret/paths/mapping.cfg create mode 100644 config/oppia-dev-workflow-remote-cache-credentials.json.secret diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f8df24103d0..89a66d3f9db 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -140,7 +140,7 @@ jobs: os: [ubuntu-18.04] steps: - uses: actions/checkout@v2 - - name: Set up bazel + - name: Set up Bazel uses: jwlawson/actions-setup-bazel@v1 with: bazel-version: '3.4.1' @@ -156,13 +156,31 @@ jobs: cd $HOME/oppia-bazel unzip bazel-tools.zip tar -xf $HOME/oppia-bazel/android_tools.tar.gz -C $GITHUB_WORKSPACE/tmp/android_tools + - name: Install git-secret (https://git-secret.io/installation) + run: | + cd $HOME + git clone https://github.com/sobolevn/git-secret.git git-secret + cd git-secret && make build + PREFIX="$HOME/gitsecret" make install + - name: Decrypt secrets + env: + GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }} + run: | + cd $HOME + # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout! + echo $GIT_SECRET_GPG_PRIVATE_KEY > ./git_secret_private_key.gpg + gpg --import ./git_secret_private_key.gpg + cd $GITHUB_WORKSPACE + git secret reveal - name: Unzip Bazel binary and build all Oppia targets + env: + BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} run: | cd $HOME/oppia-bazel unzip bazel-build.zip cd $GITHUB_WORKSPACE chmod a+x $HOME/oppia-bazel/bazel - $HOME/oppia-bazel/bazel build //... --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx + $HOME/oppia-bazel/bazel build --keep_going --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //... cp $GITHUB_WORKSPACE/bazel-bin/oppia.apk /home/runner/work/oppia-android/oppia-android/ - uses: actions/upload-artifact@v2 with: diff --git a/.gitignore b/.gitignore index 76abf6584c3..67dfc7158ef 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ utility/build gradle gradlew gradlew.bat +.gitsecret/keys/random_seed +!*.secret +config/oppia-dev-workflow-remote-cache-credentials.json diff --git a/.gitsecret/keys/pubring.kbx b/.gitsecret/keys/pubring.kbx new file mode 100644 index 0000000000000000000000000000000000000000..8727b699585657656d91e6509e24e6007522b945 GIT binary patch literal 3939 zcma)-1yodRx5v*6;7|es(lvyXfTXkxNOuY{C=5dlF*K6WC5?o{yo3VMNJy7-h=eE# zh_sXn11kN2Ue|rs{oZ@O`>b`&v(J9^IqR&o|Ns31000I80borfE+r>4y!K)|f_!N2@K2Iz z!wI@<7{?iJwR9YivK6?iJ%-Xp)f*xPpM=Lti1JU-08YcD>#cW3N-1Az<#{b>BfJ$lh_~d$iuiQ=Vs`P$Dk5rZUuaTv9B*pIo?9 zWn2N#Ah~SkI%i!%EFWEP09Lr=wzS&>c19~p#r%rhwf4c2HjB%KHv~hPjn37s%So9;IZ3*KwI%wA)zIeAcQ z^DIvw^S9^+t+bzvu&aE2X(Oe4%q|LkjpH-j`yv^R_RfnoJ~;ea$dZq6Om7h>>vR+t z2?zIu*C7vxbo0KyP_Dq?`Z>{`xJTs)X_Gl@HcID0l%D2wQE*M;3SC)#x-<$g@fv8o z>j{k#osU(rb1$!}_9nfxx?~{zVa7QN#>N&I#^2LCmta1hd);EM^&QRoQE%Q3d>IFu z^vKTR)$u?}Q3+PMwticxV_h&nVm58&y1G!6I?ZRA6(8ZcUoq?BkV8i88^<MV zi!qi1dZFjFh9;NaQ|#R7MTI2?+pm7zbdaE zh}Ua3m~VJb;uU*+8?&6-`g`SVJ3E^Gw;7A{yi|0)Tuu;tiA{C4h_+)Jh zM_~*v_?uO(9%qpbTfijfDXL=N+x47V!C{H(9kUZzO&&x|J6uEBn}sKHnnC3WJ+Wcs zj9}gm!GT8WAv%$Kr)CHCi5$b@KUpS@%gK9GYhU-U`Lvq)wwqeBQitwHYQdS_hRj?NK zb`0i!Ac~GA(NTYie(uYw8d0mJSM>6F9<=P!GRZ*d%8q;B-({rAj{GEv()lJ+82v!e zyQt0U3F#}jQA?;#P#+;~bf}!{eyK;S-%V;s@6rn6XDtSs@|o_L+g8)oJQMM!1ak$2 z_w89AwKpo~Uz3>>ybApRCp%r(cTnU+Z~n4u45$^K?%Zx%|G{pgzwP#STV)iIoU#@6 zCe<_Qkr_Dimtr%JmT~hv4rS1r< zIfosbbB(eAZ@EHdhMUdjSg)_OtnKbM*r}3EQ9QW{>ZSVB0$l>#K?S5EID}QwX?(0| z)|W=XaYbRb{7jp(hC-`;$)kg*I3HB40V8kcesKNidKE(b6D@+0J)99jKjz>=QY2kC zsIg?kObw~{S+zT&<4c==YXVS+n~-V)+N zy-k}t9%M3DQTsZSA;X6gZ>6A<-w*GC20v}W%|lxABiX~bgP>_|-v%sccaM6t2}$)q z9ThmRU-OEQj}9~9EaB4=!|Y3pZpFwa&b-^n{Fi#HEiccgPjvmX9avrRhfh$Ne2Zd# z-l#ll{Lx&@zYb)S7IyuLTk1rMq!e$u$s+22CNaUhV8_$MhA{eso7{o_^Q|)f=n##w zH-?zTPn9BCr#l%PuToX5p~V>9&yn+>CB zJsYe}3L83$QN7t402SNdwez_PyO_PpS_dMZ?HTfGSnf}%?lH02DL#L3p8j_>3tv>9 zf9-!^^W!@g$G_Q}0zGH*^LD+d!&7)Z`SiP=*EBnMqEUi@BV6MUSuK-I8NFvSR9<~4 zQHQ)KA)kqE3cVdt5+y>Kgx3J+!&=N^h)o6U$EWw)US%gUUWcW`j#pu3v!YJslTGZs zal!%-EdG}gR(3$Mlhg*R&_%Ucv7@Ahk8iexh0=mZV(1!ffpMm`D185SaHB+Z1s7`tYXOspSk#A6}l$wEqRbTbce6lS`(#iG3v4J z)xLQp^Xx4D#1uq2&hgGGV9jSZph9E{Bs12_%J6(rd8WYizh#6EvFg&_>F)PAi%+5%17AWZQ*cLq9D2 zl_vL(D1?)!ZPwwvFdf4Q$Sh%=3R|F@YS-?{l9Aa$u7&^KB7bmuum{cN0n^aiDWWbs@* zA!)cS4=k4>y17Rj$`?QIXP*4V^e^`|b>0#ja=vpq)t}W2?Q|6suNct2d_&@yl>WC5 zl*x(eiR+nTR=3Bg3}a~W8T3BT8MZhdBRmU*tDP-OarfJsR(5&4%FG^>WaE=|tq^4|Gc0`6noRS_S z%@uAYzhK6Xcg!#RfN(+!riCh*dr{Y*TiKVzNQGnx7$Tf~2;Eqm)E%mhkuq^HE>7;Y z4t=8B_ofE`CEDjYvxtGU$#1S*AK^b4@xA}uoJ*e6H0wrLFhim4%t1JiPwYNq>^8L5 z;pJOM;$9jFCo-$#$0gnu+q(MQECKo15VJR(jOv?aq#lyC#WRRD4bVrA6;j1FkSxbE z^Hk_Tcd?HHak;aZh~#xm0l91+BLhT1{$5}>(TCW=$mWxjYV)IgCRdf{Avu5l>Z=c_ z3?M4pksGlk@#zs^?AJDf9|?I#iR}0I8Bwa>e;*B9UGr{xMPY`33!6UqH%$LucKaKq z|JaJXndHpS;x>|xzkUC!@+AjER3&f5wd*5d46}`WjI8AmDjs*e!F%FZE5QE@Cbh3# z6ZlkJ&tLOdXegJc(UrWKM;iSZMk%*hGI|IPgmjaZJ>0#xbor}B8Wc*mwB>QJqKP+# zpQ%lwlPTlLs&OyZ5{fFQrFP64K?BgL91fzUYeW57YhtCxqFv1@B0RP0b6>x}&FHO= zpFOJM0Pln7UKJzAm!f-OJZ>J6@bJCAsyd#wi!&Gb#Ek8f)im9U3D`fmHS4HNW6L$D zyU5PKE0gk5|A#5pY1?vFRCc|lmwWC}4o9s9YBfx^YJcc6>qmj9%THo#t1J&ZXfv@? z((6J+b--4SyXxJ2n=X8$1EsWAp71n0^1jYz!(MKy@QQ*wtDjB_!6;*s5);l~_vM+9|j6RjLtdN|lN|cGp%dbuGEJqSSg( zJ9TTRrIxg|hT8QiUXiP)&_}(_d-LYa+~3SO=X=iiotZQ9{eA!dK!891_$OUEXW*!A z)g{{=yZ0E z1EW2f8@KJC$8!I^y8Y3y*74m@fdDQ50E-F8@}QcjRD0H73*X?6l(R5N=T(-rw35xUuuT)3q+I9l zm#zvMBT3ujSbhg1rMmk>7t_*o1qV0Z2X3aJF78WTV$DAso_!Y1$1bDY_}pnd;bA-~ zKs!>sA|ao>mq+X6id1bIa(Zv%HMtCC8l}JEnxts{a+jycU z&L2bY#JPGXI^FfW{O?!b6O-dW?;rvo4&bs767v6�)-m94x}b%+B-&0bu52XJZAk z2y(Eqae^U2U?vb)31jtlsWFYh^{q2mCm!rN-LK@?*eV#?^Uk(t&3 z#Wziz4Tf*ThO`LJT2q$g1JVXc<>7a+0SzOxSKH^)-CaEA@&0s(95=4a|aqRCITs)9^&RwMgf z42$i-l&fp3Y)?%JZ0}sw*i;}y7FBEs&Z&*vFsqPt7~Dh6y&e$8rGsHzLBVy>Iu^l9 zU4-5|tAC*1NYu5$wY^s+4b}++p`Nkyd6=94lq6H&tvk6Rq0+@LK0sBm08B^G02=>; zY?kaNu@c01?0u(&amSf!K!H)ir(z9?V)jefKvJiA^9Am0*!YJADIC^1NnXpja1e3k z1*++aG$WI%{}uubWq&l69LP; z4-2+lA)7|aAKut=NtWy%*%hNQ%6K{rYlb?J{w;Q-HoH6G0v9eBguF^T?Tg29IBilX zn+ZAkfKimimiJuCeIynolU)7U!W{kBKI$MSZUlStW2(ywReXhlHS=;b2f9Sg!{ut) zAl+VzzZ|!=L?NGhyE(!zj;U$uQB>v$?X`SFBIeF>pw~_+316PW!w)+ec}Ty3#$2*mfJjQwp`W8xRByI-;s?2wLqXpxXJttZd@n0{Wn(Wg`9`T!mfvPBIewd zZ-0*?Lf{uAiyj>&rBF!yQ?4%91Yz3D*2L)iY3Oa0g~b-4FbMiEPg?&~6vaW3fRXyh zU(d8yu1S+c`ZLVCTb{Sx+&~=an-(SVTygs2>gXIa3A$I#+8#z0SMwMduACwXVcnOo zTV>x&q<-hUR=42OE?S%isxn6Kw8@4jJzc4^zd9>y&o2=v!U`L9^XDwmDC{*}u!ak= zmhV<>4VaRI^Yo|yHu|h!vg)`-={u4!oI+MqS|lj~?)mx~4q+Vhb-+XFIZkFYIX}eTKCqFIpl}Hk2=IY%FNOv+Apyn;$4g@@RVCmL9uk+aXdx4O6}Nne!aX9h@tY^Zp27bHyBLWd~moB zSPQaFi%>f4ol0$1QJO{}zRC&h($ literal 0 HcmV?d00001 diff --git a/.gitsecret/keys/trustdb.gpg b/.gitsecret/keys/trustdb.gpg new file mode 100644 index 0000000000000000000000000000000000000000..59fe4e06e4007254bfb5639d1d9a827cca7f6d07 GIT binary patch literal 1200 zcmZQfFGy!*W@Ke#Vql2h+8@Dy9WZiX7sn7CRfiEIV1dza84VXu2#lr!%F+P<`kw@C literal 0 HcmV?d00001 diff --git a/.gitsecret/paths/mapping.cfg b/.gitsecret/paths/mapping.cfg new file mode 100644 index 00000000000..75e37c7661f --- /dev/null +++ b/.gitsecret/paths/mapping.cfg @@ -0,0 +1 @@ +config/oppia-dev-workflow-remote-cache-credentials.json:c02f95d3829f9a7fb3757170ade432efa43d562d2e7c208372ec9d0f4a45da1a diff --git a/config/oppia-dev-workflow-remote-cache-credentials.json.secret b/config/oppia-dev-workflow-remote-cache-credentials.json.secret new file mode 100644 index 0000000000000000000000000000000000000000..2aa2fc7d0fe832b376a6de32720cbed7b693f5c1 GIT binary patch literal 2543 zcmVfsGYESfYsGx^4f<dnr7f&kVoFz3Ip;+Avv2bP&gODT(wWd;UJd)AYoRAJCrT4zxlrc%q zQ2lI8Rge-o!N&MhLj-#N$WQ3(`ejuxGs>;xI@neU=D$vv`%$?pyAx_tbpEVG{VGUc z9dU+T;^Lq!cD_8Q0VZSU6y3e$2`0OEL`uQT`+M`K$S#m z3`WDEHc_gpsO{aE8nPJsz%Vu|45hh~iLT&PAx83vFBV+#AK{SROMdgqi;a=N`FI`6 z?{p%=tl1LeLw%~3J#4rg{+Gbf0>57+)Ego>8njLBf%=!FuMw8>(1ihv14r~6A7?_b zwgC(PxhUKDL#hl(z_}2_27PQYRjCgFMt^<+0x9mx;#vZ)>JQRhgunz;|M&0tbE#aW zI&e23Alh?9yMg(nLp@W#fHH@4}mR(dkF5@t%K3xfM*J#jE&zGHN9SWtIP3idwg~S z-o6S?sNys63J0N*=QEX_(Nzz+*sFD(F+of0n;>;T2X9LXcQZT^q7)}T6ZL9M!QKj8X(BdE!QEv{NzlcWcvi5fElqH0s1T5@v2rM-@kZ zys+%}w+H=-|BAP-LkvHwGLEK-y3mH5+q8HH%N z$JDrpRf$`WJ=LJN+s$#g0?Gr)b5qn#H;<0mnWr@yb%xF=o#Rx~$E#`oIyF%Xm$UVG zhU2M?Pl`s3i=KyK_+f1B#OW#b_!g;0`(8>iLOu$c10HAT%D9djZ#SttUt^T&;4_>%lZFX{ zach>;)m_t?Bw zLWEBAwo>_$TPP8bVW?7Ad0rvh5mYaQhq>cs{k+=8)92qIc>Zi|W{l)myDs9HY5Sa# zlUm4?Z!!T*$nW;@Vs!PWJrBwpA?9=R5~|jcQ-oTz!!%zeZncqYZoRxqVCKEb z1XWn0fjU&2$48{Jx=l; zvrPp+hywGG(kVe|`QanD5S18ECSP~o4@?_l3 zmMZzs5RE8~SBV>c&Pp87=jGZla?LU!y&5#M_%peCzuaRr(l#s|q~#xd;u6;V&bov% z=|}H^w0UD~`EE{-s-8xvo}-^_!fR%SpJnVjCuv_@FIPw4WN@l5nz7zdCej+8DaR%F z0)0PpAy*1X=WR_x{OzHSfN9aG;L%}bH^!Y^xcztil205s~%ny(|dxwE5y-t;{Zu*1p#uizC=a*fASpEUbej{y5SW=o3iUA27CQ< zh0Ls70?!gNgB~MLQ>aY~p0yfeHtfSaAV^v=;29kD`?{ac{v6Oa$h~uM)1fT`tZ#BO zurI)U>isXt$D}ZNmxuJ{?MkFae4%it?)jWd_Q3vT3P zO~k}5%8+LlrDtsi|Hg1u6ZybQ5{~28fLuI-B1u*_-tB>VJJo|T1)Y+hH}7)n6Hn9s zp^aZfXA&nIGlao!xu^A8v>;Z9FXg*N2=JwBt~{{mE+LL?T9jn=#M(V&CDOyDad3yK zRe9o~P$9Bb#LCZ(PFkW}-X2ifv}9-i)_YqQ1$*N?EBkEchjWy_%VQaT-2)@BaNg;} z;Zw-A8Dyv77jJtelYZJEi0{h|%MvGSnVPMNUo-71Gn1|~zMSSSKeino9Vn|HRlp~% zE8Ca}Xneq6s&=QFE5Q*aJ9L&j4b*f!J-Ph5e`-HIXq1!Cr=zg4j#=3SfNN!ITUes0 z-n{?3IZi_goChJRLFxzmho`ig#AGo{=rn|AQO~jd;5(UufL7};;h&b#i=pzcAlwAR z)JCT7elLN#h~1vs0O|lAuf)i{_#2R{%eYR0;?X#G9o-#409)S{20h4Jf-hQ0LonEg zr8fFR5B~Z@-@A#fFt61v%?H%;=qJOfwm%P0x3R8t}4QdEQu8bM?u;S+*;BMR1 zRC0^J4L`WIgarxFRz_%g##>wOr*IfC%^Zj*3d{ft>}LP92xP^gvhTj6BE84Vh4dAx zn?wvIrpgVgU}E}51I`VOZz2^ULW;wObf0#lWX%jA>a{^l8ot(&YdK@U7m=Y;^Z z=YZ|<6=c-`z}Zq=#O!s*Yhqh34fBy0ReMkFYxh117QELD0QV0~haab%{1N~iJ^j=~ z^Ng{HeH8xm?||MlNYE$QiV)tMTil<1fJS1YP)oe&O3k FFt=NW{1X5G literal 0 HcmV?d00001 From 9d026e98995c9517d60644e31ef6c139e6f26810 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 14:02:13 -0800 Subject: [PATCH 004/289] Add debug line. --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 89a66d3f9db..b224f031fca 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -170,6 +170,7 @@ jobs: # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout! echo $GIT_SECRET_GPG_PRIVATE_KEY > ./git_secret_private_key.gpg gpg --import ./git_secret_private_key.gpg + wc -c ./git_secret_private_key.gpg cd $GITHUB_WORKSPACE git secret reveal - name: Unzip Bazel binary and build all Oppia targets From 4ac50c6e23fd729012672c7a03d49064ee191eb1 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 14:04:24 -0800 Subject: [PATCH 005/289] Disable workflows + fix debug line. --- .github/workflows/main.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b224f031fca..17c8258ba7e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,6 +17,7 @@ on: jobs: linters: name: Lint Tests + if: false runs-on: ${{ matrix.os }} strategy: matrix: @@ -47,6 +48,7 @@ jobs: robolectric_tests: name: Robolectric Tests (Non-App Modules) + if: false runs-on: ${{ matrix.os }} strategy: matrix: @@ -104,6 +106,7 @@ jobs: app_tests: name: Robolectric Tests - App Module (Non-Flaky) + if: false runs-on: ${{ matrix.os }} strategy: matrix: @@ -169,8 +172,8 @@ jobs: cd $HOME # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout! echo $GIT_SECRET_GPG_PRIVATE_KEY > ./git_secret_private_key.gpg - gpg --import ./git_secret_private_key.gpg wc -c ./git_secret_private_key.gpg + gpg --import ./git_secret_private_key.gpg cd $GITHUB_WORKSPACE git secret reveal - name: Unzip Bazel binary and build all Oppia targets From 93c3608ccfef97acf12a58de30008fd52f1bebe2 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 14:12:58 -0800 Subject: [PATCH 006/289] More debugging. --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 17c8258ba7e..3ef0a29d8fc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -173,6 +173,7 @@ jobs: # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout! echo $GIT_SECRET_GPG_PRIVATE_KEY > ./git_secret_private_key.gpg wc -c ./git_secret_private_key.gpg + sha1sum ./git_secret_private_key.gpg gpg --import ./git_secret_private_key.gpg cd $GITHUB_WORKSPACE git secret reveal From 4df788f405159ba1da253fd716629dfa9f978fe4 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 14:17:35 -0800 Subject: [PATCH 007/289] More debugging. --- .github/workflows/main.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3ef0a29d8fc..c426fa8079b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -167,13 +167,15 @@ jobs: PREFIX="$HOME/gitsecret" make install - name: Decrypt secrets env: - GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }} + TEST_GPG_KEY: ${{ secrets.TEST_GPG_KEY }} + #GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }} run: | cd $HOME # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout! - echo $GIT_SECRET_GPG_PRIVATE_KEY > ./git_secret_private_key.gpg + echo $TEST_GPG_KEY > ./git_secret_private_key.gpg wc -c ./git_secret_private_key.gpg sha1sum ./git_secret_private_key.gpg + echo $TEST_GPG_KEY gpg --import ./git_secret_private_key.gpg cd $GITHUB_WORKSPACE git secret reveal From b29282684370bfb9e1c77cda52512d156c0bde77 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 14:19:57 -0800 Subject: [PATCH 008/289] Work around GitHub hiding secret since we're debugging. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c426fa8079b..1ca80c27f46 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -175,7 +175,7 @@ jobs: echo $TEST_GPG_KEY > ./git_secret_private_key.gpg wc -c ./git_secret_private_key.gpg sha1sum ./git_secret_private_key.gpg - echo $TEST_GPG_KEY + cat ./git_secret_private_key.gpg gpg --import ./git_secret_private_key.gpg cd $GITHUB_WORKSPACE git secret reveal From 5bbc72a37c91c2e035ada562a5b236fb1a1caf86 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 14:34:14 -0800 Subject: [PATCH 009/289] Use base64 to properly encode newlines in GPG keys. --- .github/workflows/main.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1ca80c27f46..cb9a8557f50 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -172,8 +172,7 @@ jobs: run: | cd $HOME # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout! - echo $TEST_GPG_KEY > ./git_secret_private_key.gpg - wc -c ./git_secret_private_key.gpg + echo $TEST_GPG_KEY | base64 --decode > ./git_secret_private_key.gpg sha1sum ./git_secret_private_key.gpg cat ./git_secret_private_key.gpg gpg --import ./git_secret_private_key.gpg From c9b9d0b32c664894e64e98d079179fca514e1211 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 14:38:30 -0800 Subject: [PATCH 010/289] Remove debug lines before changing back to correct GPG key. --- .github/workflows/main.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cb9a8557f50..98101b72d40 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -173,8 +173,6 @@ jobs: cd $HOME # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout! echo $TEST_GPG_KEY | base64 --decode > ./git_secret_private_key.gpg - sha1sum ./git_secret_private_key.gpg - cat ./git_secret_private_key.gpg gpg --import ./git_secret_private_key.gpg cd $GITHUB_WORKSPACE git secret reveal From ca3fadc0792a94a92b68ba8456746b5bdb58f40f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 14:41:13 -0800 Subject: [PATCH 011/289] Switch to production key. --- .github/workflows/main.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 98101b72d40..8ca96b0359b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -167,12 +167,11 @@ jobs: PREFIX="$HOME/gitsecret" make install - name: Decrypt secrets env: - TEST_GPG_KEY: ${{ secrets.TEST_GPG_KEY }} - #GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }} + GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }} run: | cd $HOME # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout! - echo $TEST_GPG_KEY | base64 --decode > ./git_secret_private_key.gpg + echo GIT_SECRET_GPG_PRIVATE_KEY | base64 --decode > ./git_secret_private_key.gpg gpg --import ./git_secret_private_key.gpg cd $GITHUB_WORKSPACE git secret reveal From 7854318605e61a68f9a3b18c75cc98f43e81a69d Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 14:43:06 -0800 Subject: [PATCH 012/289] Fix env variable reference. Lock-down actions workflows via codeowners. --- .github/CODEOWNERS | 3 +++ .github/workflows/main.yml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1b33b4c5b3c..dd191a30d42 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,6 +11,9 @@ # (See oppia/#10250 for an example.) Please make sure to restore ownership after # the above date passes. +# GitHub Actions & CI workflows. +.github/main.yaml @BenHenning + # Blanket codeowners # This is for the case when new files are created in any directories that aren't # covered as a whole, since in these cases, codeowners are not recognized for diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8ca96b0359b..249e75755fe 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -171,7 +171,7 @@ jobs: run: | cd $HOME # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout! - echo GIT_SECRET_GPG_PRIVATE_KEY | base64 --decode > ./git_secret_private_key.gpg + echo $GIT_SECRET_GPG_PRIVATE_KEY | base64 --decode > ./git_secret_private_key.gpg gpg --import ./git_secret_private_key.gpg cd $GITHUB_WORKSPACE git secret reveal From 74a775b367ff9d9a1678d5edab766f4f81053069 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 14:45:47 -0800 Subject: [PATCH 013/289] Install git-secret to default location. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 249e75755fe..a2dcdfcbabe 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -164,7 +164,7 @@ jobs: cd $HOME git clone https://github.com/sobolevn/git-secret.git git-secret cd git-secret && make build - PREFIX="$HOME/gitsecret" make install + make install - name: Decrypt secrets env: GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }} From dfa98744db9a0d1e9ac4467e8960bfdb10b13584 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 14:47:52 -0800 Subject: [PATCH 014/289] Add details. Debug $PATH. --- .github/workflows/main.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a2dcdfcbabe..45fccdc1e3e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -159,9 +159,14 @@ jobs: cd $HOME/oppia-bazel unzip bazel-tools.zip tar -xf $HOME/oppia-bazel/android_tools.tar.gz -C $GITHUB_WORKSPACE/tmp/android_tools - - name: Install git-secret (https://git-secret.io/installation) + # See https://git-secret.io/installation for details on installing git-secret. Note that the + # apt-get method isn't used since it's much slower to update & upgrade apt before installation + # versus just directly cloning & installing the project. Further, the specific version + # shouldn't matter since git-secret relies on a future-proof storage mechanism for secrets. + - name: Install git-secret run: | cd $HOME + echo $PATH git clone https://github.com/sobolevn/git-secret.git git-secret cd git-secret && make build make install From 3064c24931458cd8f854b3146140b7ac90ae89a9 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 14:53:24 -0800 Subject: [PATCH 015/289] Fix pathing for git-secret. --- .github/workflows/main.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 45fccdc1e3e..742eeceb7c8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -163,13 +163,16 @@ jobs: # apt-get method isn't used since it's much slower to update & upgrade apt before installation # versus just directly cloning & installing the project. Further, the specific version # shouldn't matter since git-secret relies on a future-proof storage mechanism for secrets. + # This also uses a different directory to install git-secret to avoid requiring root access + # when running the git secret command. - name: Install git-secret run: | cd $HOME - echo $PATH + mkdir -p $HOME/gitsecret + echo "$HOME/gitsecret" >> $GITHUB_PATH git clone https://github.com/sobolevn/git-secret.git git-secret cd git-secret && make build - make install + PREFIX="$HOME/gitsecret" make install - name: Decrypt secrets env: GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }} From 2fd86631459063bef12334b8661331b054b491e4 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 14:57:34 -0800 Subject: [PATCH 016/289] Dummy commit to re-trigger actions. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 742eeceb7c8..888b524426a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -164,7 +164,7 @@ jobs: # versus just directly cloning & installing the project. Further, the specific version # shouldn't matter since git-secret relies on a future-proof storage mechanism for secrets. # This also uses a different directory to install git-secret to avoid requiring root access - # when running the git secret command. + # when running the git secret command.. - name: Install git-secret run: | cd $HOME From e099d9d926dcd40a32d9e39db4d91fb29bb9aa03 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 15:00:48 -0800 Subject: [PATCH 017/289] Undo commit to see if this one shows up. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 888b524426a..742eeceb7c8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -164,7 +164,7 @@ jobs: # versus just directly cloning & installing the project. Further, the specific version # shouldn't matter since git-secret relies on a future-proof storage mechanism for secrets. # This also uses a different directory to install git-secret to avoid requiring root access - # when running the git secret command.. + # when running the git secret command. - name: Install git-secret run: | cd $HOME From 54e7996cbcbe36db382b3f2560043c4b2c5d501c Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 15:02:58 -0800 Subject: [PATCH 018/289] Fix git-secret pathing try 2. --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 742eeceb7c8..562f7c48830 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -169,10 +169,11 @@ jobs: run: | cd $HOME mkdir -p $HOME/gitsecret - echo "$HOME/gitsecret" >> $GITHUB_PATH git clone https://github.com/sobolevn/git-secret.git git-secret cd git-secret && make build PREFIX="$HOME/gitsecret" make install + echo "$HOME/gitsecret" >> $GITHUB_PATH + echo "$HOME/gitsecret/bin" >> $GITHUB_PATH - name: Decrypt secrets env: GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }} From 17a4cf2ae2e7dac9fea37d1fae84264edd304513 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 15:04:53 -0800 Subject: [PATCH 019/289] New commit to re-trigger action. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 562f7c48830..ee01254dfac 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -164,7 +164,7 @@ jobs: # versus just directly cloning & installing the project. Further, the specific version # shouldn't matter since git-secret relies on a future-proof storage mechanism for secrets. # This also uses a different directory to install git-secret to avoid requiring root access - # when running the git secret command. + # when running the git secret command.. - name: Install git-secret run: | cd $HOME From 5013d2e8238edbb471d0ff9949c8dc6e2c5476f3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 15:06:06 -0800 Subject: [PATCH 020/289] Path debugging. --- .github/workflows/main.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ee01254dfac..b3cb3b13138 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -164,7 +164,7 @@ jobs: # versus just directly cloning & installing the project. Further, the specific version # shouldn't matter since git-secret relies on a future-proof storage mechanism for secrets. # This also uses a different directory to install git-secret to avoid requiring root access - # when running the git secret command.. + # when running the git secret command. - name: Install git-secret run: | cd $HOME @@ -174,6 +174,8 @@ jobs: PREFIX="$HOME/gitsecret" make install echo "$HOME/gitsecret" >> $GITHUB_PATH echo "$HOME/gitsecret/bin" >> $GITHUB_PATH + echo "Path:" + echo $PATH - name: Decrypt secrets env: GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }} @@ -183,6 +185,8 @@ jobs: echo $GIT_SECRET_GPG_PRIVATE_KEY | base64 --decode > ./git_secret_private_key.gpg gpg --import ./git_secret_private_key.gpg cd $GITHUB_WORKSPACE + echo "Path:" + echo $PATH git secret reveal - name: Unzip Bazel binary and build all Oppia targets env: From ed344b045b94ab96472a56308222115c07071bc8 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 15:07:54 -0800 Subject: [PATCH 021/289] Workaround to get GitHub to show changes. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b3cb3b13138..8a876ecdbda 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -164,7 +164,7 @@ jobs: # versus just directly cloning & installing the project. Further, the specific version # shouldn't matter since git-secret relies on a future-proof storage mechanism for secrets. # This also uses a different directory to install git-secret to avoid requiring root access - # when running the git secret command. + # when running the git secret command.. - name: Install git-secret run: | cd $HOME From dce41332c1475188d74487ed2c60c2f4588c2f2f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 15:12:43 -0800 Subject: [PATCH 022/289] Update runner to use Bash. Reference: https://github.community/t/github-path-does-not-add-to-the-path/143992. --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8a876ecdbda..c6af17a3c9b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -164,8 +164,9 @@ jobs: # versus just directly cloning & installing the project. Further, the specific version # shouldn't matter since git-secret relies on a future-proof storage mechanism for secrets. # This also uses a different directory to install git-secret to avoid requiring root access - # when running the git secret command.. + # when running the git secret command. - name: Install git-secret + shell: bash run: | cd $HOME mkdir -p $HOME/gitsecret From 8794e18ac0d01e255952e73f44cdb0934552a618 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 16:19:23 -0800 Subject: [PATCH 023/289] Restore binary-only build, other builds, and introduce new workflow for building all Bazel targets. --- .github/workflows/main.yml | 40 ++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c6af17a3c9b..d3818d19bea 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,7 +17,6 @@ on: jobs: linters: name: Lint Tests - if: false runs-on: ${{ matrix.os }} strategy: matrix: @@ -48,7 +47,6 @@ jobs: robolectric_tests: name: Robolectric Tests (Non-App Modules) - if: false runs-on: ${{ matrix.os }} strategy: matrix: @@ -106,7 +104,6 @@ jobs: app_tests: name: Robolectric Tests - App Module (Non-Flaky) - if: false runs-on: ${{ matrix.os }} strategy: matrix: @@ -147,6 +144,42 @@ jobs: uses: jwlawson/actions-setup-bazel@v1 with: bazel-version: '3.4.1' + - name: Clone Oppia Bazel + run: git clone https://github.com/oppia/bazel.git $HOME/oppia-bazel + - name: Set up JDK 9 + uses: actions/setup-java@v1 + with: + java-version: 9 + - name: Extract Android tools + run: | + mkdir -p $GITHUB_WORKSPACE/tmp/android_tools + cd $HOME/oppia-bazel + unzip bazel-tools.zip + tar -xf $HOME/oppia-bazel/android_tools.tar.gz -C $GITHUB_WORKSPACE/tmp/android_tools + - name: Unzip Bazel binary and build all Oppia targets + run: | + cd $HOME/oppia-bazel + unzip bazel-build.zip + cd $GITHUB_WORKSPACE + chmod a+x $HOME/oppia-bazel/bazel + $HOME/oppia-bazel/bazel build --keep_going --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx -- //:oppia + cp $GITHUB_WORKSPACE/bazel-bin/oppia.apk /home/runner/work/oppia-android/oppia-android/ + - uses: actions/upload-artifact@v2 + with: + name: oppia-bazel-apk + path: /home/runner/work/oppia-android/oppia-android/oppia.apk + - uses: actions/checkout@v2 + + bazel_build_all_with_caching: + name: Build All Bazel Targets (with caching) + # Caching only works for workflows started on non-forks since it requires access to secrets. + if: github.repository == 'oppia/oppia-android' + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-18.04] + steps: + - uses: actions/checkout@v2 - name: Clone Oppia Bazel run: git clone https://github.com/oppia/bazel.git $HOME/oppia-bazel - name: Set up JDK 9 @@ -204,4 +237,3 @@ jobs: name: oppia-bazel-apk path: /home/runner/work/oppia-android/oppia-android/oppia.apk - uses: actions/checkout@v2 - From f7b10f254736319edcaf122a9ea4f5a33cb1bef5 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 18 Nov 2020 16:22:14 -0800 Subject: [PATCH 024/289] Remove debug lines. --- .github/workflows/main.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d3818d19bea..8f382a738b2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -208,8 +208,6 @@ jobs: PREFIX="$HOME/gitsecret" make install echo "$HOME/gitsecret" >> $GITHUB_PATH echo "$HOME/gitsecret/bin" >> $GITHUB_PATH - echo "Path:" - echo $PATH - name: Decrypt secrets env: GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }} @@ -219,8 +217,6 @@ jobs: echo $GIT_SECRET_GPG_PRIVATE_KEY | base64 --decode > ./git_secret_private_key.gpg gpg --import ./git_secret_private_key.gpg cd $GITHUB_WORKSPACE - echo "Path:" - echo $PATH git secret reveal - name: Unzip Bazel binary and build all Oppia targets env: From 9cbd94c3eb9c5966cf2175140ade805ab0cf9a67 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 19 Nov 2020 15:37:03 -0800 Subject: [PATCH 025/289] Rename & remove keep_going. --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8f382a738b2..e82b6f76b72 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -156,13 +156,13 @@ jobs: cd $HOME/oppia-bazel unzip bazel-tools.zip tar -xf $HOME/oppia-bazel/android_tools.tar.gz -C $GITHUB_WORKSPACE/tmp/android_tools - - name: Unzip Bazel binary and build all Oppia targets + - name: Unzip Bazel binary and build Oppia binary run: | cd $HOME/oppia-bazel unzip bazel-build.zip cd $GITHUB_WORKSPACE chmod a+x $HOME/oppia-bazel/bazel - $HOME/oppia-bazel/bazel build --keep_going --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx -- //:oppia + $HOME/oppia-bazel/bazel build --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx -- //:oppia cp $GITHUB_WORKSPACE/bazel-bin/oppia.apk /home/runner/work/oppia-android/oppia-android/ - uses: actions/upload-artifact@v2 with: From 2aa09446b21f464b2b4bf9c63ad0989781f7d743 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 19 Nov 2020 18:07:52 -0800 Subject: [PATCH 026/289] Compute matrix containing all test targets. This will allow us to distribute parallelization responsibilities partly to GitHub actions to hopefully have slightly better throughput. See https://github.blog/changelog/2020-04-15-github-actions-new-workflow-features/ for reference on how this mechanism works. --- .github/workflows/main.yml | 76 ++++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e82b6f76b72..e0897e4dfdf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -170,14 +170,13 @@ jobs: path: /home/runner/work/oppia-android/oppia-android/oppia.apk - uses: actions/checkout@v2 - bazel_build_all_with_caching: - name: Build All Bazel Targets (with caching) + bazel_build_binary_with_caching: + name: Build Bazel Binary (with caching) # Caching only works for workflows started on non-forks since it requires access to secrets. if: github.repository == 'oppia/oppia-android' - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-18.04] + runs-on: ubuntu-18.04 + outputs: + matrix: ${{ steps.compute-test-matrix.outputs.matrix }} steps: - uses: actions/checkout@v2 - name: Clone Oppia Bazel @@ -226,10 +225,73 @@ jobs: unzip bazel-build.zip cd $GITHUB_WORKSPACE chmod a+x $HOME/oppia-bazel/bazel - $HOME/oppia-bazel/bazel build --keep_going --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //... + $HOME/oppia-bazel/bazel build --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:oppia cp $GITHUB_WORKSPACE/bazel-bin/oppia.apk /home/runner/work/oppia-android/oppia-android/ + - id: compute-test-matrix + # TODO(#1861): update the query below to compute only affected targets (more useful after + # the codebase is more split up). + # https://unix.stackexchange.com/a/338124 for reference on creating a JSON-friendly + # comma-separated list of test targets for the matrix. + run: | + TEST_TARGET_LIST=$($HOME/oppia-bazel/bazel query "tests(//...)" 2>/dev/null | sed 's/^\|$/"/g' | paste -sd, -) + echo "Test target list: $TEST_TARGET_LIST" + echo "::set-output name=matrix::{\"test-target\":[$TEST_TARGET_LIST]}" - uses: actions/upload-artifact@v2 with: name: oppia-bazel-apk path: /home/runner/work/oppia-android/oppia-android/oppia.apk - uses: actions/checkout@v2 + + bazel_run_test_with_caching: + name: Run Bazel Test (with caching) + # Caching only works for workflows started on non-forks since it requires access to secrets. + if: github.repository == 'oppia/oppia-android' + needs: bazel_build_binary_with_caching + runs-on: ubuntu-18.04 + strategy: + matrix: ${{fromJson(needs.bazel_build_binary_with_caching.outputs.matrix}} + steps: + - uses: actions/checkout@v2 + - name: Clone Oppia Bazel + run: git clone https://github.com/oppia/bazel.git $HOME/oppia-bazel + - name: Set up JDK 9 + uses: actions/setup-java@v1 + with: + java-version: 9 + - name: Extract Android tools + run: | + mkdir -p $GITHUB_WORKSPACE/tmp/android_tools + cd $HOME/oppia-bazel + unzip bazel-tools.zip + tar -xf $HOME/oppia-bazel/android_tools.tar.gz -C $GITHUB_WORKSPACE/tmp/android_tools + # See explanation in bazel_build_binary_with_caching for how this is installed. + - name: Install git-secret + shell: bash + run: | + cd $HOME + mkdir -p $HOME/gitsecret + git clone https://github.com/sobolevn/git-secret.git git-secret + cd git-secret && make build + PREFIX="$HOME/gitsecret" make install + echo "$HOME/gitsecret" >> $GITHUB_PATH + echo "$HOME/gitsecret/bin" >> $GITHUB_PATH + - name: Decrypt secrets + env: + GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }} + run: | + cd $HOME + # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout! + echo $GIT_SECRET_GPG_PRIVATE_KEY | base64 --decode > ./git_secret_private_key.gpg + gpg --import ./git_secret_private_key.gpg + cd $GITHUB_WORKSPACE + git secret reveal + - name: Unzip Bazel binary and build all Oppia targets + env: + BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} + run: | + cd $HOME/oppia-bazel + unzip bazel-build.zip + cd $GITHUB_WORKSPACE + chmod a+x $HOME/oppia-bazel/bazel + $HOME/oppia-bazel/bazel test --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:${{ matrix.test-target }} + - uses: actions/checkout@v2 From 226c4a1788dd3b8d4ab00df96a3965a75583fb55 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 19 Nov 2020 18:12:17 -0800 Subject: [PATCH 027/289] Simplify, fix some issues, and debug instead of run. --- .github/workflows/main.yml | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e0897e4dfdf..22524fd8390 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,10 +17,8 @@ on: jobs: linters: name: Lint Tests - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-18.04] + if: false + runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 @@ -47,10 +45,8 @@ jobs: robolectric_tests: name: Robolectric Tests (Non-App Modules) - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-18.04] + if: false + runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 - uses: actions/cache@v2 @@ -104,11 +100,9 @@ jobs: app_tests: name: Robolectric Tests - App Module (Non-Flaky) - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-18.04] - steps: + if: false + runs-on: ubuntu-18.04 + steps: - uses: actions/checkout@v2 - uses: actions/cache@v2 @@ -220,13 +214,13 @@ jobs: - name: Unzip Bazel binary and build all Oppia targets env: BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} + #$HOME/oppia-bazel/bazel build --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:oppia + #cp $GITHUB_WORKSPACE/bazel-bin/oppia.apk /home/runner/work/oppia-android/oppia-android/ run: | cd $HOME/oppia-bazel unzip bazel-build.zip cd $GITHUB_WORKSPACE chmod a+x $HOME/oppia-bazel/bazel - $HOME/oppia-bazel/bazel build --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:oppia - cp $GITHUB_WORKSPACE/bazel-bin/oppia.apk /home/runner/work/oppia-android/oppia-android/ - id: compute-test-matrix # TODO(#1861): update the query below to compute only affected targets (more useful after # the codebase is more split up). @@ -249,7 +243,7 @@ jobs: needs: bazel_build_binary_with_caching runs-on: ubuntu-18.04 strategy: - matrix: ${{fromJson(needs.bazel_build_binary_with_caching.outputs.matrix}} + matrix: ${{fromJson(needs.bazel_build_binary_with_caching.outputs.matrix)}} steps: - uses: actions/checkout@v2 - name: Clone Oppia Bazel @@ -288,10 +282,11 @@ jobs: - name: Unzip Bazel binary and build all Oppia targets env: BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} + #$HOME/oppia-bazel/bazel test --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- ${{ matrix.test-target }} run: | cd $HOME/oppia-bazel unzip bazel-build.zip cd $GITHUB_WORKSPACE chmod a+x $HOME/oppia-bazel/bazel - $HOME/oppia-bazel/bazel test --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:${{ matrix.test-target }} + echo ${{ matrix.test-target }} - uses: actions/checkout@v2 From 7ae7e5ffea5ea07bd536b85c9dae9c45eca92485 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 19 Nov 2020 18:24:52 -0800 Subject: [PATCH 028/289] Turn on actual testing. --- .github/workflows/main.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 22524fd8390..5963de02a2e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -128,6 +128,7 @@ jobs: bazel_build_app: name: Build Binary with Bazel + if: false runs-on: ${{ matrix.os }} strategy: matrix: @@ -214,14 +215,15 @@ jobs: - name: Unzip Bazel binary and build all Oppia targets env: BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} - #$HOME/oppia-bazel/bazel build --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:oppia - #cp $GITHUB_WORKSPACE/bazel-bin/oppia.apk /home/runner/work/oppia-android/oppia-android/ run: | cd $HOME/oppia-bazel unzip bazel-build.zip cd $GITHUB_WORKSPACE chmod a+x $HOME/oppia-bazel/bazel - - id: compute-test-matrix + $HOME/oppia-bazel/bazel build --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:oppia + cp $GITHUB_WORKSPACE/bazel-bin/oppia.apk /home/runner/work/oppia-android/oppia-android/ + - name: Compute test matrix to parallelize in GitHub Actions + id: compute-test-matrix # TODO(#1861): update the query below to compute only affected targets (more useful after # the codebase is more split up). # https://unix.stackexchange.com/a/338124 for reference on creating a JSON-friendly @@ -282,11 +284,10 @@ jobs: - name: Unzip Bazel binary and build all Oppia targets env: BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} - #$HOME/oppia-bazel/bazel test --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- ${{ matrix.test-target }} run: | cd $HOME/oppia-bazel unzip bazel-build.zip cd $GITHUB_WORKSPACE chmod a+x $HOME/oppia-bazel/bazel - echo ${{ matrix.test-target }} + $HOME/oppia-bazel/bazel test --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- ${{ matrix.test-target }} - uses: actions/checkout@v2 From f26276e23775676544fc05fd057ee4c22bde005c Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 19 Nov 2020 19:08:51 -0800 Subject: [PATCH 029/289] Lower parallelization since GitHub started cancelling tasks. --- .github/workflows/main.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5963de02a2e..2330b9cfa58 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -212,7 +212,7 @@ jobs: gpg --import ./git_secret_private_key.gpg cd $GITHUB_WORKSPACE git secret reveal - - name: Unzip Bazel binary and build all Oppia targets + - name: Unzip Bazel binary and build Oppia binary env: BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} run: | @@ -245,6 +245,7 @@ jobs: needs: bazel_build_binary_with_caching runs-on: ubuntu-18.04 strategy: + max-parallel: 25 matrix: ${{fromJson(needs.bazel_build_binary_with_caching.outputs.matrix)}} steps: - uses: actions/checkout@v2 @@ -281,7 +282,7 @@ jobs: gpg --import ./git_secret_private_key.gpg cd $GITHUB_WORKSPACE git secret reveal - - name: Unzip Bazel binary and build all Oppia targets + - name: Unzip Bazel binary and run Oppia test env: BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} run: | From 1850784a18e595220cf6c23c4e2b0229a8ae24a5 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 19 Nov 2020 19:39:42 -0800 Subject: [PATCH 030/289] Try 15 parallel jobs instead. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2330b9cfa58..0893a5304a3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -245,7 +245,7 @@ jobs: needs: bazel_build_binary_with_caching runs-on: ubuntu-18.04 strategy: - max-parallel: 25 + max-parallel: 15 matrix: ${{fromJson(needs.bazel_build_binary_with_caching.outputs.matrix)}} steps: - uses: actions/checkout@v2 From 49b3584f531c2e0d23667878a27e382cc8bf831e Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 19 Nov 2020 21:14:11 -0800 Subject: [PATCH 031/289] Turn off fail-fast instead of limiting parallelization. Fail fast seems to be the reason why the tests aren't completing, not quota (since if too many jobs are started, the extras should just be queued until resources open up). --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0893a5304a3..6cfce036531 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -245,7 +245,7 @@ jobs: needs: bazel_build_binary_with_caching runs-on: ubuntu-18.04 strategy: - max-parallel: 15 + fail-fast: false matrix: ${{fromJson(needs.bazel_build_binary_with_caching.outputs.matrix)}} steps: - uses: actions/checkout@v2 From c1546cb1749ba49d958d82718e2c612dbb18d879 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 20 Nov 2020 10:50:34 -0800 Subject: [PATCH 032/289] Simplify workflow & allow it to be required. Also, introduce bazelrc file to simplify the CI CLIs interacting with Bazel. --- .bazelrc | 6 +++ .github/workflows/main.yml | 97 +++++++++++++++----------------------- 2 files changed, 43 insertions(+), 60 deletions(-) create mode 100644 .bazelrc diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 00000000000..4d31c933410 --- /dev/null +++ b/.bazelrc @@ -0,0 +1,6 @@ +# Configurations for arguments that should automatically be added to Bazel commands. +build --android_databinding_use_v3_4_args \ + --experimental_android_databinding_v2 \ + --java_header_compilation=false \ + --noincremental_dexing \ + --define=android_standalone_dexing_tool=d8_compat_dx diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6cfce036531..0c064744396 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -127,48 +127,7 @@ jobs: path: app/build/reports bazel_build_app: - name: Build Binary with Bazel - if: false - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-18.04] - steps: - - uses: actions/checkout@v2 - - name: Set up Bazel - uses: jwlawson/actions-setup-bazel@v1 - with: - bazel-version: '3.4.1' - - name: Clone Oppia Bazel - run: git clone https://github.com/oppia/bazel.git $HOME/oppia-bazel - - name: Set up JDK 9 - uses: actions/setup-java@v1 - with: - java-version: 9 - - name: Extract Android tools - run: | - mkdir -p $GITHUB_WORKSPACE/tmp/android_tools - cd $HOME/oppia-bazel - unzip bazel-tools.zip - tar -xf $HOME/oppia-bazel/android_tools.tar.gz -C $GITHUB_WORKSPACE/tmp/android_tools - - name: Unzip Bazel binary and build Oppia binary - run: | - cd $HOME/oppia-bazel - unzip bazel-build.zip - cd $GITHUB_WORKSPACE - chmod a+x $HOME/oppia-bazel/bazel - $HOME/oppia-bazel/bazel build --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx -- //:oppia - cp $GITHUB_WORKSPACE/bazel-bin/oppia.apk /home/runner/work/oppia-android/oppia-android/ - - uses: actions/upload-artifact@v2 - with: - name: oppia-bazel-apk - path: /home/runner/work/oppia-android/oppia-android/oppia.apk - - uses: actions/checkout@v2 - - bazel_build_binary_with_caching: - name: Build Bazel Binary (with caching) - # Caching only works for workflows started on non-forks since it requires access to secrets. - if: github.repository == 'oppia/oppia-android' + name: Build Bazel Binary runs-on: ubuntu-18.04 outputs: matrix: ${{ steps.compute-test-matrix.outputs.matrix }} @@ -192,7 +151,8 @@ jobs: # shouldn't matter since git-secret relies on a future-proof storage mechanism for secrets. # This also uses a different directory to install git-secret to avoid requiring root access # when running the git secret command. - - name: Install git-secret + - name: Install git-secret (non-fork only) + if: github.repository == 'oppia/oppia-android' shell: bash run: | cd $HOME @@ -202,7 +162,8 @@ jobs: PREFIX="$HOME/gitsecret" make install echo "$HOME/gitsecret" >> $GITHUB_PATH echo "$HOME/gitsecret/bin" >> $GITHUB_PATH - - name: Decrypt secrets + - name: Decrypt secrets (non-fork only) + if: github.repository == 'oppia/oppia-android' env: GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }} run: | @@ -212,16 +173,25 @@ jobs: gpg --import ./git_secret_private_key.gpg cd $GITHUB_WORKSPACE git secret reveal - - name: Unzip Bazel binary and build Oppia binary - env: - BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} + - name: Unzip Bazel binary run: | cd $HOME/oppia-bazel unzip bazel-build.zip cd $GITHUB_WORKSPACE chmod a+x $HOME/oppia-bazel/bazel - $HOME/oppia-bazel/bazel build --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:oppia - cp $GITHUB_WORKSPACE/bazel-bin/oppia.apk /home/runner/work/oppia-android/oppia-android/ + # Note that caching only works on non-forks. + - name: Build Oppia binary (with caching, non-fork only) + if: github.repository == 'oppia/oppia-android' + env: + BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} + run: | + $HOME/oppia-bazel/bazel build --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:oppia + - name: Build Oppia binary (without caching, fork only) + if: github.repository != 'oppia/oppia-android' + run: | + $HOME/oppia-bazel/bazel build --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools -- //:oppia + - name: Copy Oppia APK for uploading + run: cp $GITHUB_WORKSPACE/bazel-bin/oppia.apk /home/runner/work/oppia-android/oppia-android/ - name: Compute test matrix to parallelize in GitHub Actions id: compute-test-matrix # TODO(#1861): update the query below to compute only affected targets (more useful after @@ -240,13 +210,11 @@ jobs: bazel_run_test_with_caching: name: Run Bazel Test (with caching) - # Caching only works for workflows started on non-forks since it requires access to secrets. - if: github.repository == 'oppia/oppia-android' - needs: bazel_build_binary_with_caching + needs: bazel_build_app runs-on: ubuntu-18.04 strategy: fail-fast: false - matrix: ${{fromJson(needs.bazel_build_binary_with_caching.outputs.matrix)}} + matrix: ${{fromJson(needs.bazel_build_app.outputs.matrix)}} steps: - uses: actions/checkout@v2 - name: Clone Oppia Bazel @@ -261,8 +229,9 @@ jobs: cd $HOME/oppia-bazel unzip bazel-tools.zip tar -xf $HOME/oppia-bazel/android_tools.tar.gz -C $GITHUB_WORKSPACE/tmp/android_tools - # See explanation in bazel_build_binary_with_caching for how this is installed. - - name: Install git-secret + # See explanation in bazel_build_app for how this is installed. + - name: Install git-secret (non-fork only) + if: github.repository == 'oppia/oppia-android' shell: bash run: | cd $HOME @@ -272,7 +241,8 @@ jobs: PREFIX="$HOME/gitsecret" make install echo "$HOME/gitsecret" >> $GITHUB_PATH echo "$HOME/gitsecret/bin" >> $GITHUB_PATH - - name: Decrypt secrets + - name: Decrypt secrets (non-fork only) + if: github.repository == 'oppia/oppia-android' env: GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }} run: | @@ -282,13 +252,20 @@ jobs: gpg --import ./git_secret_private_key.gpg cd $GITHUB_WORKSPACE git secret reveal - - name: Unzip Bazel binary and run Oppia test - env: - BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} + - name: Unzip Bazel binary run: | cd $HOME/oppia-bazel unzip bazel-build.zip cd $GITHUB_WORKSPACE chmod a+x $HOME/oppia-bazel/bazel - $HOME/oppia-bazel/bazel test --android_databinding_use_v3_4_args --experimental_android_databinding_v2 --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --java_header_compilation=false --noincremental_dexing --define=android_standalone_dexing_tool=d8_compat_dx --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- ${{ matrix.test-target }} + - name: Run Oppia Test (with caching, non-fork only) + if: github.repository == 'oppia/oppia-android' + env: + BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} + run: $HOME/oppia-bazel/bazel test --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- ${{ matrix.test-target }} + - name: Run Oppia Test (without caching, fork only) + if: github.repository != 'oppia/oppia-android' + env: + BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} + run: $HOME/oppia-bazel/bazel test --test_output=all --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools -- ${{ matrix.test-target }} - uses: actions/checkout@v2 From dcda5834759c0564887b8ee514f0b089ce527d1d Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 20 Nov 2020 10:51:12 -0800 Subject: [PATCH 033/289] Add test change to investigate computing affected targets. --- .../oppia/android/app/player/state/StateFragmentLocalTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt index 8823299d0bf..9a4eadd5e09 100644 --- a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt @@ -136,6 +136,8 @@ class StateFragmentLocalTest { private val internalProfileId: Int = 1 private val solutionIndex: Int = 4 + // extra line to indicate the test changed + @Before fun setUp() { setUpTestApplicationComponent() From 4aff1a88b02a2dba57b51ee436f52efe8ffdb47b Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 20 Nov 2020 10:55:11 -0800 Subject: [PATCH 034/289] Another test change to compute affected targets. --- .../app/player/state/StatePlayerRecyclerViewAssembler.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt index 6204e1ed35d..2f932425ead 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt @@ -134,6 +134,8 @@ class StatePlayerRecyclerViewAssembler private constructor( delayShowAdditionalHintsMs: Long, delayShowAdditionalHintsFromWrongAnswerMs: Long ) : HtmlParser.CustomOppiaTagActionListener { + // Test change to see what tests this affects. + /** * A list of view models corresponding to past view models that are hidden by default. These are * intentionally not retained upon configuration changes since the user can just re-expand the From d5904c1c60aa385106fe9d6d7e95a7d1b2fbacba Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 20 Nov 2020 11:50:57 -0800 Subject: [PATCH 035/289] Update workflow to use future script compute_affected_tests.sh. Also, ignore Bazel directories in Git (to ease local development). --- .github/workflows/main.yml | 4 ++-- .gitignore | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0c064744396..74b905ac7bd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -199,8 +199,8 @@ jobs: # https://unix.stackexchange.com/a/338124 for reference on creating a JSON-friendly # comma-separated list of test targets for the matrix. run: | - TEST_TARGET_LIST=$($HOME/oppia-bazel/bazel query "tests(//...)" 2>/dev/null | sed 's/^\|$/"/g' | paste -sd, -) - echo "Test target list: $TEST_TARGET_LIST" + TEST_TARGET_LIST=$(bash ./scripts/compute_affected_tests.sh $HOME/oppia-bazel/bazel | sed 's/^\|$/"/g' | paste -sd, -) + echo "Affected tests (note that this might be all tests if on the develop branch): $TEST_TARGET_LIST" echo "::set-output name=matrix::{\"test-target\":[$TEST_TARGET_LIST]}" - uses: actions/upload-artifact@v2 with: diff --git a/.gitignore b/.gitignore index 67dfc7158ef..fe10fc61e64 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ gradlew.bat .gitsecret/keys/random_seed !*.secret config/oppia-dev-workflow-remote-cache-credentials.json +bazel-* From 479d683c1ab586dac88d6d81554143b3adef5ed7 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 20 Nov 2020 11:59:19 -0800 Subject: [PATCH 036/289] Add script to compute affected targets. This script is primarily meant to be used for CI. --- scripts/compute_affected_tests.sh | 49 +++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100755 scripts/compute_affected_tests.sh diff --git a/scripts/compute_affected_tests.sh b/scripts/compute_affected_tests.sh new file mode 100755 index 00000000000..8271eabbf8b --- /dev/null +++ b/scripts/compute_affected_tests.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Compute the list of tests that are affected by changes on this branch (including in the working +# directory). This script is useful to verify that only the tests that could break due to a change +# are actually verified as passing. Note that this script does not actually run any of the tests. +# +# Usage: +# bash scripts/compute_affected_tests.sh +# +# This script is based on https://github.com/bazelbuild/bazel/blob/d96e1cd/scripts/ci/ci.sh and the +# documentation at https://docs.bazel.build/versions/master/query.html. Note that this script will +# automatically list all tests if it's run on the develop branch (the idea being that test +# considerations on the develop branch should always consider all targets). + +BAZEL_BINARY=$1 + +# Reference: https://stackoverflow.com/a/6245587. +current_branch=$(git branch --show-current) + +if [ "$current_branch" != "develop" ]; then + # https://stackoverflow.com/a/9294015 for constructing the arrays. + commit_range=HEAD..$(git merge-base origin/develop HEAD) + changed_committed_files=($(git diff --name-only $commit_range)) + changed_staged_files=($(git diff --name-only --cached)) + changed_unstaged_files=($(git diff --name-only)) + # See https://stackoverflow.com/a/35484355 for how this works. + changed_untracked_files=($(git ls-files --others --exclude-standard)) + + changed_files_with_potential_duplicates=( + "${changed_committed_files[@]}" + "${changed_staged_files[@]}" + "${changed_unstaged_files[@]}" + "${changed_untracked_files[@]}" + ) + # De-duplicated files: https://unix.stackexchange.com/q/377812. + changed_files=($(printf "%s\n" "${changed_files_with_potential_duplicates[@]}" | sort -u)) + + # Filter all of the source files among those that are actually included in Bazel builds. + changed_bazel_files=() + for changed_file in ${changed_files[@]}; do + changed_bazel_files+=($($BAZEL_BINARY query --noshow_progress $changed_file 2> /dev/null)) + done + + # Compute the list of affected tests. + $BAZEL_BINARY query --noshow_progress "kind(test, rdeps(//..., set(${changed_bazel_files[@]})))" 2>/dev/null +else + # Print all test targets. + $BAZEL_BINARY query --noshow_progress "kind(test, //...)" 2>/dev/null +fi From c2c9fcd5c8dbde3627b7dafce1900c68b70ed7e3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 20 Nov 2020 12:03:49 -0800 Subject: [PATCH 037/289] Execute tests in parallel to build. This creates a new job to compute affected targets alongside the build. This may result in the initial build being a bit slower, but subsequent commits should be fast if remote caching is enabled. This will also result in better performance for forks that can't use remote caching. --- .github/workflows/main.yml | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 74b905ac7bd..3904d89e27a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -129,8 +129,6 @@ jobs: bazel_build_app: name: Build Bazel Binary runs-on: ubuntu-18.04 - outputs: - matrix: ${{ steps.compute-test-matrix.outputs.matrix }} steps: - uses: actions/checkout@v2 - name: Clone Oppia Bazel @@ -192,6 +190,26 @@ jobs: $HOME/oppia-bazel/bazel build --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools -- //:oppia - name: Copy Oppia APK for uploading run: cp $GITHUB_WORKSPACE/bazel-bin/oppia.apk /home/runner/work/oppia-android/oppia-android/ + - uses: actions/upload-artifact@v2 + with: + name: oppia-bazel-apk + path: /home/runner/work/oppia-android/oppia-android/oppia.apk + + bazel_compute_affected_targets: + name: Compute affected tests + runs-on: ubuntu-18.04 + outputs: + matrix: ${{ steps.compute-test-matrix.outputs.matrix }} + steps: + - uses: actions/checkout@v2 + - name: Clone Oppia Bazel + run: git clone https://github.com/oppia/bazel.git $HOME/oppia-bazel + - name: Unzip Bazel binary + run: | + cd $HOME/oppia-bazel + unzip bazel-build.zip + cd $GITHUB_WORKSPACE + chmod a+x $HOME/oppia-bazel/bazel - name: Compute test matrix to parallelize in GitHub Actions id: compute-test-matrix # TODO(#1861): update the query below to compute only affected targets (more useful after @@ -202,19 +220,14 @@ jobs: TEST_TARGET_LIST=$(bash ./scripts/compute_affected_tests.sh $HOME/oppia-bazel/bazel | sed 's/^\|$/"/g' | paste -sd, -) echo "Affected tests (note that this might be all tests if on the develop branch): $TEST_TARGET_LIST" echo "::set-output name=matrix::{\"test-target\":[$TEST_TARGET_LIST]}" - - uses: actions/upload-artifact@v2 - with: - name: oppia-bazel-apk - path: /home/runner/work/oppia-android/oppia-android/oppia.apk - - uses: actions/checkout@v2 - bazel_run_test_with_caching: - name: Run Bazel Test (with caching) - needs: bazel_build_app + bazel_run_test: + name: Run Bazel Test + needs: bazel_compute_affected_targets runs-on: ubuntu-18.04 strategy: fail-fast: false - matrix: ${{fromJson(needs.bazel_build_app.outputs.matrix)}} + matrix: ${{fromJson(needs.bazel_compute_affected_targets.outputs.matrix)}} steps: - uses: actions/checkout@v2 - name: Clone Oppia Bazel @@ -268,4 +281,3 @@ jobs: env: BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} run: $HOME/oppia-bazel/bazel test --test_output=all --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools -- ${{ matrix.test-target }} - - uses: actions/checkout@v2 From 372f2f45563d69485d2b660099f341e7ac914e46 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 20 Nov 2020 12:14:48 -0800 Subject: [PATCH 038/289] Script clean-ups. Also, re-trigger actions. --- scripts/compute_affected_tests.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/compute_affected_tests.sh b/scripts/compute_affected_tests.sh index 8271eabbf8b..782a5795d77 100755 --- a/scripts/compute_affected_tests.sh +++ b/scripts/compute_affected_tests.sh @@ -18,7 +18,8 @@ BAZEL_BINARY=$1 current_branch=$(git branch --show-current) if [ "$current_branch" != "develop" ]; then - # https://stackoverflow.com/a/9294015 for constructing the arrays. + # Compute all files that have been changed on this branch. https://stackoverflow.com/a/9294015 for + # constructing the arrays. commit_range=HEAD..$(git merge-base origin/develop HEAD) changed_committed_files=($(git diff --name-only $commit_range)) changed_staged_files=($(git diff --name-only --cached)) @@ -32,7 +33,8 @@ if [ "$current_branch" != "develop" ]; then "${changed_unstaged_files[@]}" "${changed_untracked_files[@]}" ) - # De-duplicated files: https://unix.stackexchange.com/q/377812. + + # De-duplicate files: https://unix.stackexchange.com/q/377812. changed_files=($(printf "%s\n" "${changed_files_with_potential_duplicates[@]}" | sort -u)) # Filter all of the source files among those that are actually included in Bazel builds. From 5c888de2b83a0560465ca80089a840d2785c059a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 20 Nov 2020 14:06:15 -0800 Subject: [PATCH 039/289] Try to ensure develop branch is available for change analysis. --- .github/workflows/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3904d89e27a..559df3161af 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -129,6 +129,7 @@ jobs: bazel_build_app: name: Build Bazel Binary runs-on: ubuntu-18.04 + if: false steps: - uses: actions/checkout@v2 - name: Clone Oppia Bazel @@ -202,6 +203,8 @@ jobs: matrix: ${{ steps.compute-test-matrix.outputs.matrix }} steps: - uses: actions/checkout@v2 + with: + fetch-depth: 0 - name: Clone Oppia Bazel run: git clone https://github.com/oppia/bazel.git $HOME/oppia-bazel - name: Unzip Bazel binary From 0bd553244efabd56fadf087f808c829b093e8832 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 20 Nov 2020 14:43:18 -0800 Subject: [PATCH 040/289] Add automatic workflow cancellation. Also, add support to explicitly run all tests if '[RunAllTests]' is in the PR title. --- .github/workflows/main.yml | 12 +++++++++--- .github/workflows/workflow_canceller.yml | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/workflow_canceller.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 559df3161af..7f25c902bd3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -213,16 +213,22 @@ jobs: unzip bazel-build.zip cd $GITHUB_WORKSPACE chmod a+x $HOME/oppia-bazel/bazel - - name: Compute test matrix to parallelize in GitHub Actions + - name: Compute test matrix based on affected targets id: compute-test-matrix - # TODO(#1861): update the query below to compute only affected targets (more useful after - # the codebase is more split up). + if: "!contains(github.event.pull_request.title, '[RunAllTests]')" # https://unix.stackexchange.com/a/338124 for reference on creating a JSON-friendly # comma-separated list of test targets for the matrix. run: | TEST_TARGET_LIST=$(bash ./scripts/compute_affected_tests.sh $HOME/oppia-bazel/bazel | sed 's/^\|$/"/g' | paste -sd, -) echo "Affected tests (note that this might be all tests if on the develop branch): $TEST_TARGET_LIST" echo "::set-output name=matrix::{\"test-target\":[$TEST_TARGET_LIST]}" + - name: Compute test matrix based on all tests + id: compute-test-matrix + if: "contains(github.event.pull_request.title, '[RunAllTests]')" + run: | + TEST_TARGET_LIST=$($HOME/oppia-bazel/bazel query "kind(test, //...)" | sed 's/^\|$/"/g' | paste -sd, -) + echo "Affected tests (note that this might be all tests if on the develop branch): $TEST_TARGET_LIST" + echo "::set-output name=matrix::{\"test-target\":[$TEST_TARGET_LIST]}" bazel_run_test: name: Run Bazel Test diff --git a/.github/workflows/workflow_canceller.yml b/.github/workflows/workflow_canceller.yml new file mode 100644 index 00000000000..b3402485e7a --- /dev/null +++ b/.github/workflows/workflow_canceller.yml @@ -0,0 +1,19 @@ +name: Automatic Workflow Canceller + +on: + workflow_dispatch: + pull_request: + push: + branches: + # Push events on develop branch + - develop + +jobs: + cancel: + name: Cancel Previous Runs + runs-on: ubuntu-18.04 + steps: + - uses: styfle/cancel-workflow-action@0.6.0 + with: + workflow_id: main.yml + access_token: ${{ github.token }} From fe0fec6c2977506c910c983a91f7c235e1e894c5 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 20 Nov 2020 14:48:46 -0800 Subject: [PATCH 041/289] Attempt to make conditional matrix computation work. --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7f25c902bd3..40b7f38d85e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -200,7 +200,7 @@ jobs: name: Compute affected tests runs-on: ubuntu-18.04 outputs: - matrix: ${{ steps.compute-test-matrix.outputs.matrix }} + matrix: ${{ join(steps.compute-test-matrix-from-affected.outputs.matrix, steps.compute-test-matrix-from-all.outputs.matrix) }} steps: - uses: actions/checkout@v2 with: @@ -214,7 +214,7 @@ jobs: cd $GITHUB_WORKSPACE chmod a+x $HOME/oppia-bazel/bazel - name: Compute test matrix based on affected targets - id: compute-test-matrix + id: compute-test-matrix-from-affected if: "!contains(github.event.pull_request.title, '[RunAllTests]')" # https://unix.stackexchange.com/a/338124 for reference on creating a JSON-friendly # comma-separated list of test targets for the matrix. @@ -223,7 +223,7 @@ jobs: echo "Affected tests (note that this might be all tests if on the develop branch): $TEST_TARGET_LIST" echo "::set-output name=matrix::{\"test-target\":[$TEST_TARGET_LIST]}" - name: Compute test matrix based on all tests - id: compute-test-matrix + id: compute-test-matrix-from-all if: "contains(github.event.pull_request.title, '[RunAllTests]')" run: | TEST_TARGET_LIST=$($HOME/oppia-bazel/bazel query "kind(test, //...)" | sed 's/^\|$/"/g' | paste -sd, -) From 1155a9f13598cf434a6a371ae8388fe0ece0baab Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 20 Nov 2020 15:03:39 -0800 Subject: [PATCH 042/289] Remove join since it was used incorrectly. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 40b7f38d85e..8b12c7475c5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -200,7 +200,7 @@ jobs: name: Compute affected tests runs-on: ubuntu-18.04 outputs: - matrix: ${{ join(steps.compute-test-matrix-from-affected.outputs.matrix, steps.compute-test-matrix-from-all.outputs.matrix) }} + matrix: ${{ steps.compute-test-matrix-from-affected.outputs.matrix || steps.compute-test-matrix-from-all.outputs.matrix }} steps: - uses: actions/checkout@v2 with: From 1b25e34341a48b61de4ea07971258f8f102890cd Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 20 Nov 2020 16:42:12 -0800 Subject: [PATCH 043/289] Add support for testing when Bazel, BUILD, and WORKSPACE files change. One consequence is the current Bazel file structure is very tight, so any changes will likely run the whole suite. This will get better over time. Also, show test logs for all test runs. --- .bazelrc | 3 +++ .github/workflows/main.yml | 2 +- scripts/compute_affected_tests.sh | 32 ++++++++++++++++++++++++++++--- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/.bazelrc b/.bazelrc index 4d31c933410..71793d2ba7c 100644 --- a/.bazelrc +++ b/.bazelrc @@ -4,3 +4,6 @@ build --android_databinding_use_v3_4_args \ --java_header_compilation=false \ --noincremental_dexing \ --define=android_standalone_dexing_tool=d8_compat_dx + +# Show all test output by default (for better debugging). +test --test_output=all diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8b12c7475c5..277db738671 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -289,4 +289,4 @@ jobs: if: github.repository != 'oppia/oppia-android' env: BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} - run: $HOME/oppia-bazel/bazel test --test_output=all --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools -- ${{ matrix.test-target }} + run: $HOME/oppia-bazel/bazel test --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools -- ${{ matrix.test-target }} diff --git a/scripts/compute_affected_tests.sh b/scripts/compute_affected_tests.sh index 782a5795d77..0b4990a47e0 100755 --- a/scripts/compute_affected_tests.sh +++ b/scripts/compute_affected_tests.sh @@ -17,7 +17,7 @@ BAZEL_BINARY=$1 # Reference: https://stackoverflow.com/a/6245587. current_branch=$(git branch --show-current) -if [ "$current_branch" != "develop" ]; then +if [[ "$current_branch" != "develop" ]]; then # Compute all files that have been changed on this branch. https://stackoverflow.com/a/9294015 for # constructing the arrays. commit_range=HEAD..$(git merge-base origin/develop HEAD) @@ -43,8 +43,34 @@ if [ "$current_branch" != "develop" ]; then changed_bazel_files+=($($BAZEL_BINARY query --noshow_progress $changed_file 2> /dev/null)) done - # Compute the list of affected tests. - $BAZEL_BINARY query --noshow_progress "kind(test, rdeps(//..., set(${changed_bazel_files[@]})))" 2>/dev/null + # Compute the list of affected tests based on source files. + source_affected_targets=$($BAZEL_BINARY query --noshow_progress --universe_scope=//... --order_output=no "kind(test, allrdeps(set(${changed_bazel_files[@]})))" 2>/dev/null) + + # Compute the list of files to consider for BUILD-level changes (this uses the base file list as a + # reference since Bazel's query won't find matching targets for utility bzl files that can still + # affect the build). https://stackoverflow.com/a/44107086 for reference on changing case matching. + shopt -s nocasematch + changed_bazel_support_files=() + for changed_file in ${changed_files[@]}; do + if [[ "$changed_file" =~ ^.+?\.bazel$ ]] || [[ "$changed_file" =~ ^.+?\.bzl$ ]] || [[ "$changed_file" == "WORKSPACE" ]]; then + changed_bazel_support_files+=($changed_file) + fi + done + shopt -u nocasematch + + # Compute the list of affected tests based on BUILD/Bazel/WORKSPACE files. These are generally + # framed as: if a BUILD file changes, run all tests transitively connected to it. + # Reference for joining an array to string: https://stackoverflow.com/a/53839433. + printf -v changed_bazel_support_files_list '%s,' "${changed_bazel_support_files[@]}" + build_affected_targets=$($BAZEL_BINARY query --noshow_progress --universe_scope=//... --order_output=no "filter('^[^@]', kind(test, allrdeps(siblings(rbuildfiles(${changed_bazel_support_files_list%,})))))" 2>/dev/null) + + all_affected_targets_with_potential_duplicated=( + "${source_affected_targets[@]}" + "${build_affected_targets[@]}" + ) + + # Print all of the affected targets without duplicates. + printf "%s\n" "${all_affected_targets_with_potential_duplicated[@]}" | sort -u else # Print all test targets. $BAZEL_BINARY query --noshow_progress "kind(test, //...)" 2>/dev/null From 86c5c93bb7eac6e05fd41bb0ebb2801d66fa3442 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 20 Nov 2020 16:49:17 -0800 Subject: [PATCH 044/289] Fix broken tests by adding missing dep library. --- domain/BUILD.bazel | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index 2ab358b4514..9ee45784aca 100644 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -23,9 +23,22 @@ kt_android_library( ], ) +# TODO(#2143): Move InteractionObjectTestBuilder to a testing package outside the test folder. +kt_android_library( + name = "interaction_object_test_builder", + testonly = True, + srcs = [ + "src/test/java/org/oppia/android/domain/classify/InteractionObjectTestBuilder.kt", + ], + deps = [ + "//model", + ], +) + TEST_DEPS = [ ":dagger", ":domain", + ":interaction_object_test_builder", "//data:persistent_cache_store", "//model", "//testing", From 7ed040ed8700ec6116985db0d2354973f74d0446 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 20 Nov 2020 18:44:07 -0800 Subject: [PATCH 045/289] Finalize PR. 1. Expand codeowners to include all workflow files. 2. Remove test comments in Kotlin files. 3. Re-enable all workflows. 4. Attempt to fix tests broken on actions but not locally by adding more thread synchronization points. --- .github/CODEOWNERS | 2 +- .github/workflows/main.yml | 4 --- .../state/StatePlayerRecyclerViewAssembler.kt | 2 -- .../player/state/StateFragmentLocalTest.kt | 32 ++++++++++++++++--- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index dd191a30d42..75bc528e7cd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -12,7 +12,7 @@ # the above date passes. # GitHub Actions & CI workflows. -.github/main.yaml @BenHenning +.github/*.yaml @BenHenning # Blanket codeowners # This is for the case when new files are created in any directories that aren't diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 277db738671..2e64e2b5e7d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,7 +17,6 @@ on: jobs: linters: name: Lint Tests - if: false runs-on: ubuntu-18.04 steps: @@ -45,7 +44,6 @@ jobs: robolectric_tests: name: Robolectric Tests (Non-App Modules) - if: false runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 @@ -100,7 +98,6 @@ jobs: app_tests: name: Robolectric Tests - App Module (Non-Flaky) - if: false runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 @@ -129,7 +126,6 @@ jobs: bazel_build_app: name: Build Bazel Binary runs-on: ubuntu-18.04 - if: false steps: - uses: actions/checkout@v2 - name: Clone Oppia Bazel diff --git a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt index 2f932425ead..6204e1ed35d 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt @@ -134,8 +134,6 @@ class StatePlayerRecyclerViewAssembler private constructor( delayShowAdditionalHintsMs: Long, delayShowAdditionalHintsFromWrongAnswerMs: Long ) : HtmlParser.CustomOppiaTagActionListener { - // Test change to see what tests this affects. - /** * A list of view models corresponding to past view models that are hidden by default. These are * intentionally not retained upon configuration changes since the user can just re-expand the diff --git a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt index 9a4eadd5e09..6b4938e7193 100644 --- a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt @@ -54,6 +54,7 @@ import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.CONTINUE_NAVIGATION_BUTTON import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.FRACTION_INPUT_INTERACTION import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.NEXT_NAVIGATION_BUTTON +import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.PREVIOUS_NAVIGATION_BUTTON import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.PREVIOUS_RESPONSES_HEADER import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.SELECTION_INTERACTION import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.SUBMIT_ANSWER_BUTTON @@ -136,8 +137,6 @@ class StateFragmentLocalTest { private val internalProfileId: Int = 1 private val solutionIndex: Int = 4 - // extra line to indicate the test changed - @Before fun setUp() { setUpTestApplicationComponent() @@ -288,6 +287,8 @@ class StateFragmentLocalTest { submitTwoWrongAnswers() onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(PREVIOUS_RESPONSES_HEADER)) + testCoroutineDispatchers.runCurrent() + onView(withId(R.id.previous_response_header)).check(matches(isDisplayed())) } } @@ -300,6 +301,8 @@ class StateFragmentLocalTest { submitTwoWrongAnswers() onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(PREVIOUS_RESPONSES_HEADER)) + testCoroutineDispatchers.runCurrent() + onView(withId(R.id.previous_response_header)).check(matches(isDisplayed())) onView(withId(R.id.state_recycler_view)).check(matches(hasChildCount(/* childCount= */ 5))) } @@ -314,7 +317,9 @@ class StateFragmentLocalTest { submitTwoWrongAnswers() onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(PREVIOUS_RESPONSES_HEADER)) + testCoroutineDispatchers.runCurrent() onView(withId(R.id.previous_response_header)).perform(click()) + testCoroutineDispatchers.runCurrent() onView(withId(R.id.state_recycler_view)).check(matches(hasChildCount(/* childCount= */ 6))) } } @@ -328,13 +333,18 @@ class StateFragmentLocalTest { submitTwoWrongAnswers() onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(PREVIOUS_RESPONSES_HEADER)) + testCoroutineDispatchers.runCurrent() onView(withId(R.id.previous_response_header)).check(matches(isDisplayed())) onView(withId(R.id.state_recycler_view)).check(matches(hasChildCount(/* childCount= */ 5))) onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(PREVIOUS_RESPONSES_HEADER)) + testCoroutineDispatchers.runCurrent() onView(withSubstring("Previous Responses")).perform(click()) + testCoroutineDispatchers.runCurrent() onView(withId(R.id.state_recycler_view)).check(matches(hasChildCount(/* childCount= */ 6))) onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(PREVIOUS_RESPONSES_HEADER)) + testCoroutineDispatchers.runCurrent() onView(withSubstring("Previous Responses")).perform(click()) + testCoroutineDispatchers.runCurrent() onView(withId(R.id.state_recycler_view)).check(matches(hasChildCount(/* childCount= */ 5))) } } @@ -400,6 +410,9 @@ class StateFragmentLocalTest { playThroughState1() submitTwoWrongAnswers() onView(withId(R.id.hint_bulb)).check(matches(isDisplayed())) + // The previous navigation button is next to a submit answer button in this state. + onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(SUBMIT_ANSWER_BUTTON)) + testCoroutineDispatchers.runCurrent() onView(withId(R.id.previous_state_navigation_button)).perform(click()) testCoroutineDispatchers.runCurrent() onView(withId(R.id.hint_bulb)).check(matches(not(isDisplayed()))) @@ -413,7 +426,7 @@ class StateFragmentLocalTest { playThroughState1() submitTwoWrongAnswers() onView(withId(R.id.dot_hint)).check(matches(isDisplayed())) - moveToPreviousAndBackToCurrentState() + moveToPreviousAndBackToCurrentStateWithSubmitButton() onView(withId(R.id.dot_hint)).check(matches(isDisplayed())) } } @@ -430,7 +443,7 @@ class StateFragmentLocalTest { onView(withText("Reveal Hint")).inRoot(isDialog()).check(matches(isDisplayed())) closeHintsAndSolutionsDialog() - moveToPreviousAndBackToCurrentState() + moveToPreviousAndBackToCurrentStateWithSubmitButton() openHintsAndSolutionsDialog() onView(withText("Hint 1")).inRoot(isDialog()).check(matches(isDisplayed())) @@ -449,6 +462,7 @@ class StateFragmentLocalTest { onView(withId(R.id.previous_state_navigation_button)).perform(click()) testCoroutineDispatchers.runCurrent() onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(NEXT_NAVIGATION_BUTTON)) + testCoroutineDispatchers.runCurrent() onView(withId(R.id.next_state_navigation_button)).perform(click()) testCoroutineDispatchers.runCurrent() @@ -456,6 +470,7 @@ class StateFragmentLocalTest { onView(withId(R.id.hints_and_solution_recycler_view)) .inRoot(isDialog()) .perform(scrollToPosition(0)) + testCoroutineDispatchers.runCurrent() onView( RecyclerViewMatcher.atPositionOnView( R.id.hints_and_solution_recycler_view, 0, R.id.hint_summary_container @@ -711,6 +726,7 @@ class StateFragmentLocalTest { onView(withId(R.id.hints_and_solution_recycler_view)) .inRoot(isDialog()) .perform(scrollToPosition(/* position= */ solutionIndex * 2)) + testCoroutineDispatchers.runCurrent() onView(allOf(withId(R.id.reveal_solution_button), isDisplayed())) .inRoot(isDialog()) .check(matches(isDisplayed())) @@ -763,6 +779,7 @@ class StateFragmentLocalTest { onView(withId(R.id.hints_and_solution_recycler_view)) .inRoot(isDialog()) .perform(scrollToPosition(/* position= */ solutionIndex * 2)) + testCoroutineDispatchers.runCurrent() onView(allOf(withId(R.id.reveal_solution_button), isDisplayed())) .inRoot(isDialog()) .check(matches(isDisplayed())) @@ -786,9 +803,11 @@ class StateFragmentLocalTest { onView(withId(R.id.hints_and_solution_recycler_view)) .inRoot(isDialog()) .perform(scrollToPosition(/* position= */ solutionIndex * 2)) + testCoroutineDispatchers.runCurrent() onView(allOf(withId(R.id.reveal_solution_button), isDisplayed())) .inRoot(isDialog()) .perform(click()) + testCoroutineDispatchers.runCurrent() onView(withText("This will reveal the solution. Are you sure?")) .inRoot(isDialog()) @@ -1248,7 +1267,10 @@ class StateFragmentLocalTest { } // Go to previous state and then come back to current state - private fun moveToPreviousAndBackToCurrentState() { + private fun moveToPreviousAndBackToCurrentStateWithSubmitButton() { + // The previous navigation button is bundled with the submit button sometimes, and specifically + // for tests that are currently on a state with a submit button after the first state. + onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(SUBMIT_ANSWER_BUTTON)) onView(withId(R.id.previous_state_navigation_button)).perform(click()) testCoroutineDispatchers.runCurrent() onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(NEXT_NAVIGATION_BUTTON)) From ba45fd923ee5373fbcbdce7bf4885972d9c7e832 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 20 Nov 2020 18:46:57 -0800 Subject: [PATCH 046/289] Lint fix. --- .../org/oppia/android/app/player/state/StateFragmentLocalTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt index 6b4938e7193..51f586f90f2 100644 --- a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt @@ -54,7 +54,6 @@ import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.CONTINUE_NAVIGATION_BUTTON import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.FRACTION_INPUT_INTERACTION import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.NEXT_NAVIGATION_BUTTON -import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.PREVIOUS_NAVIGATION_BUTTON import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.PREVIOUS_RESPONSES_HEADER import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.SELECTION_INTERACTION import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.SUBMIT_ANSWER_BUTTON From d7fd5bb6108a73340432a9e82c0f9dff2daf441b Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 23 Nov 2020 13:51:41 -0800 Subject: [PATCH 047/289] Fix timing issues and JDK 9-specific regression. See comment thread in #1904 for more context. --- .../res/layout-land/submitted_answer_item.xml | 1 + .../main/res/layout/submitted_answer_item.xml | 1 + .../player/state/StateFragmentLocalTest.kt | 87 ++++++++++++++++--- 3 files changed, 75 insertions(+), 14 deletions(-) diff --git a/app/src/main/res/layout-land/submitted_answer_item.xml b/app/src/main/res/layout-land/submitted_answer_item.xml index be47a4839fb..ea3947a6437 100644 --- a/app/src/main/res/layout-land/submitted_answer_item.xml +++ b/app/src/main/res/layout-land/submitted_answer_item.xml @@ -20,6 +20,7 @@ , times: Int): ViewAssertion { + return matches( + object : TypeSafeMatcher() { + override fun describeTo(description: Description?) { + description + ?.appendDescriptionOf(matcher) + ?.appendText(" occurs times: $times in child views") + } + + override fun matchesSafely(view: View?): Boolean { + if (view !is ViewGroup) { + throw PerformException.Builder() + .withCause(IllegalStateException("Expected to match against view group, not: $view")) + .build() + } + val matchingCount = view.children.filter(matcher::matches).count() + if (matchingCount != times) { + throw PerformException.Builder() + .withActionDescription("Expected to match $matcher against $times children") + .withViewDescription("$view") + .withCause( + IllegalStateException("Matched $matchingCount times in $view (expected $times)") + ) + .build() + } + return true + } + }) + } + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi. @Singleton From 80a60398c47a0eeb42522f11e344f961da41b65a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 23 Nov 2020 14:08:33 -0800 Subject: [PATCH 048/289] Restore workflow names + introduce test check. The new test check workflow will be a static job that will be required to pass. One failing test is being introduced to verify that this check fails as expected. The original workflow names are being restored so that they don't need to be renamed in GitHub settings (since that would introduce a discontinuity in CI service & require multiple migratiaon PRs to fix). --- .github/workflows/main.yml | 33 ++++++++++++++++--- .../player/state/StateFragmentLocalTest.kt | 5 +++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2e64e2b5e7d..b96e29b22f8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,7 +17,10 @@ on: jobs: linters: name: Lint Tests - runs-on: ubuntu-18.04 + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-18.04] steps: - uses: actions/checkout@v2 @@ -44,7 +47,10 @@ jobs: robolectric_tests: name: Robolectric Tests (Non-App Modules) - runs-on: ubuntu-18.04 + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-18.04] steps: - uses: actions/checkout@v2 - uses: actions/cache@v2 @@ -98,7 +104,10 @@ jobs: app_tests: name: Robolectric Tests - App Module (Non-Flaky) - runs-on: ubuntu-18.04 + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-18.04] steps: - uses: actions/checkout@v2 @@ -124,8 +133,11 @@ jobs: path: app/build/reports bazel_build_app: - name: Build Bazel Binary - runs-on: ubuntu-18.04 + name: Build Binary with Bazel + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-18.04] steps: - uses: actions/checkout@v2 - name: Clone Oppia Bazel @@ -286,3 +298,14 @@ jobs: env: BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} run: $HOME/oppia-bazel/bazel test --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools -- ${{ matrix.test-target }} + + # Reference: https://github.community/t/127354/7. + check_test_results: + name: Check Bazel Test Results + needs: bazel_run_test + if: ${{ always() }} + runs-on: ubuntu-18.04 + steps: + - name: Check tests passed + if: ${{ needs.bazel_run_test.result != 'success' }} + run: exit 1 diff --git a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt index 4a6d0f51b58..9c558e92544 100644 --- a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt @@ -256,6 +256,11 @@ class StateFragmentLocalTest { } } + @Test + fun testFailOnPurpose() { + throw AssertionError("Failed") + } + @Test fun testStateFragment_nextState_wait120seconds_canViewOneHint() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { From 528bf6bc4b3e99d94bcf83572eb8906474ded555 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 23 Nov 2020 16:46:37 -0800 Subject: [PATCH 049/289] Update StateFragmentLocalTest.kt Remove fail-on-purpose test since it verified what it needed to: the new test status check job is working as expected. --- .../oppia/android/app/player/state/StateFragmentLocalTest.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt index 9c558e92544..4a6d0f51b58 100644 --- a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt @@ -256,11 +256,6 @@ class StateFragmentLocalTest { } } - @Test - fun testFailOnPurpose() { - throw AssertionError("Failed") - } - @Test fun testStateFragment_nextState_wait120seconds_canViewOneHint() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { From f338ea6c36d7af385a61db11e3569548d0d82178 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 26 Nov 2020 00:32:55 -0800 Subject: [PATCH 050/289] Initial commit for algeraic expression support. This commit introduces: 1. Lexer for math expressions with single-letter variables. 2. Math expression parsing (no functions or unary support yet). 3. Polynomial computation engine (division not yet working, and no collecting like terms or general form computation). This commit does not include (but these may be needed in future commits): 1. Evaluation (though this is pretty easy to do later on). 2. Explicit equality/equivalence checks for expressions (equality may be possible with an exact tree match, maybe with some allowances for certain equivalence deviations. Equivalence can be done by comparing the resulting polynomials for the two expressions also converted to general form). 3. Partial expression tree matching (I'm not entirely sure how to solve this yet. Simply looking for a subtree won't work since the tree's order is based on order of operations which can change based on operations surrounding a particular sub-expression). 4. Equation matching (I *think* this can be done by computing an expression that = 0 by subtracting the left-hand side with the right-hand side, then performing the polynomial-level checks. This will also require additional parsing support for the right-associative equivalence operator: =). Extra noteworthy parts: 1. The lexer + parser are pretty efficient. Polynomial computation is expensive due to its inherent complexity. 2. During polynomial computation, fractions/whole numbers are kept as long as possible to avoid floats being passed around. Note that this commit is introducing the system in a pretty rough state. The final code organization, documentation, and even tests aren't yet finalized here. This commit should only be considered a proof-of-concept. --- .bazelproject | 12 + domain/BUILD.bazel | 1 + ...AndInSimplestFormRuleClassifierProvider.kt | 6 +- ...putIsEquivalentToRuleClassifierProvider.kt | 4 +- ...nputIsGreaterThanRuleClassifierProvider.kt | 2 +- ...onInputIsLessThanRuleClassifierProvider.kt | 2 +- ...ithUnitsIsEqualToRuleClassifierProvider.kt | 2 +- ...itsIsEquivalentToRuleClassifierProvider.kt | 4 +- ...umericInputEqualsRuleClassifierProvider.kt | 2 +- .../android/domain/util/FractionExtensions.kt | 30 - .../android/domain/util/RatioExtensions.kt | 2 +- ...icInputEqualsRuleClassifierProviderTest.kt | 2 +- model/BUILD.bazel | 20 +- .../proto/format_import_proto_library.bzl | 3 +- model/src/main/proto/interaction_object.proto | 10 +- model/src/main/proto/math.proto | 79 +++ utility/BUILD.bazel | 40 +- .../android/util/math}/FloatExtensions.kt | 2 +- .../android/util/math/FractionExtensions.kt | 196 +++++++ .../util/math/MathExpressionExtensions.kt | 544 ++++++++++++++++++ .../android/util/math/MathExpressionParser.kt | 236 ++++++++ .../oppia/android/util/math/MathTokenizer.kt | 308 ++++++++++ .../util/math/MathExpressionParserTest.kt | 66 +++ .../android/util/math/MathTokenizerTest.kt | 286 +++++++++ 24 files changed, 1802 insertions(+), 57 deletions(-) create mode 100644 .bazelproject delete mode 100644 domain/src/main/java/org/oppia/android/domain/util/FractionExtensions.kt create mode 100644 model/src/main/proto/math.proto rename {domain/src/main/java/org/oppia/android/domain/util => utility/src/main/java/org/oppia/android/util/math}/FloatExtensions.kt (93%) create mode 100644 utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt diff --git a/.bazelproject b/.bazelproject new file mode 100644 index 00000000000..a4dc7774572 --- /dev/null +++ b/.bazelproject @@ -0,0 +1,12 @@ +directories: + . + +derive_targets_from_directories: true + +additional_languages: + kotlin + +android_sdk_platform: android-29 + +#bazel_binary: /usr/local/google/home/bhenning/bazel-dev2/bazel-bin/src/bazel +bazel_binary: /usr/local/google/home/bhenning/patched-bazel/bazel diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index 9ee45784aca..9df86c4c56a 100644 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -19,6 +19,7 @@ kt_android_library( deps = [ ":dagger", "//data:persistent_cache_store", + "//utility:math", artifact("androidx.work:work-runtime-ktx:2.4.0"), ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt index bffab3f96d6..06896e5c93a 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt @@ -5,9 +5,9 @@ import org.oppia.android.app.model.InteractionObject import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals -import org.oppia.android.domain.util.toFloat -import org.oppia.android.domain.util.toSimplestForm +import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.toFloat +import org.oppia.android.util.math.toSimplestForm import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt index 3f327c293f3..7957ca2ed4b 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt @@ -5,8 +5,8 @@ import org.oppia.android.app.model.InteractionObject import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals -import org.oppia.android.domain.util.toFloat +import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.toFloat import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt index 7f6134b77c2..1eccb28813f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt @@ -5,7 +5,7 @@ import org.oppia.android.app.model.InteractionObject import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.toFloat +import org.oppia.android.util.math.toFloat import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt index 2af4756865e..74cfe09d1d4 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt @@ -5,7 +5,7 @@ import org.oppia.android.app.model.InteractionObject import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.toFloat +import org.oppia.android.util.math.toFloat import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt index 4fa9de516df..78e2edbf797 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt @@ -6,7 +6,7 @@ import org.oppia.android.app.model.NumberWithUnits import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals +import org.oppia.android.util.math.approximatelyEquals import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt index e5d66332c9d..0295106e28c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt @@ -5,8 +5,8 @@ import org.oppia.android.app.model.NumberWithUnits import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals -import org.oppia.android.domain.util.toFloat +import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.toFloat import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt index df4dc9c591f..38ea5e095ff 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt @@ -4,7 +4,7 @@ import org.oppia.android.app.model.InteractionObject import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals +import org.oppia.android.util.math.approximatelyEquals import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/util/FractionExtensions.kt b/domain/src/main/java/org/oppia/android/domain/util/FractionExtensions.kt deleted file mode 100644 index 878576da012..00000000000 --- a/domain/src/main/java/org/oppia/android/domain/util/FractionExtensions.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.oppia.android.domain.util - -import org.oppia.android.app.model.Fraction - -/** - * Returns a float version of this fraction. - * - * See: https://github.com/oppia/oppia/blob/37285a/core/templates/dev/head/domain/objects/FractionObjectFactory.ts#L73. - */ -fun Fraction.toFloat(): Float { - val totalParts = ((wholeNumber * denominator) + numerator).toFloat() - val floatVal = totalParts / denominator.toFloat() - return if (isNegative) -floatVal else floatVal -} - -/** - * Returns this fraction in its most simplified form. - * - * See: https://github.com/oppia/oppia/blob/37285a/core/templates/dev/head/domain/objects/FractionObjectFactory.ts#L83. - */ -fun Fraction.toSimplestForm(): Fraction { - val commonDenominator = gcd(numerator, denominator) - return toBuilder().setNumerator(numerator / commonDenominator) - .setDenominator(denominator / commonDenominator).build() -} - -/** Returns the greatest common divisor between two integers. */ -fun gcd(x: Int, y: Int): Int { - return if (y == 0) x else gcd(y, x % y) -} diff --git a/domain/src/main/java/org/oppia/android/domain/util/RatioExtensions.kt b/domain/src/main/java/org/oppia/android/domain/util/RatioExtensions.kt index 821fa274e31..9a698c306ae 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/RatioExtensions.kt +++ b/domain/src/main/java/org/oppia/android/domain/util/RatioExtensions.kt @@ -9,7 +9,7 @@ fun RatioExpression.toSimplestForm(): List { return if (this.ratioComponentList.contains(0)) { this.ratioComponentList } else { - val gcdComponentResult = this.ratioComponentList.reduce { x, y -> gcd(x, y) } + val gcdComponentResult = this.ratioComponentList.reduce { x, y -> org.oppia.android.util.math.gcd(x, y) } this.ratioComponentList.map { x -> x / gcdComponentResult } } } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt index d6c92a6a3f1..f02f2af2be8 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt @@ -10,7 +10,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject -import org.oppia.android.domain.util.FLOAT_EQUALITY_INTERVAL +import org.oppia.android.util.math.FLOAT_EQUALITY_INTERVAL import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject diff --git a/model/BUILD.bazel b/model/BUILD.bazel index 0012cee603b..057d5909758 100644 --- a/model/BUILD.bazel +++ b/model/BUILD.bazel @@ -56,9 +56,12 @@ java_lite_proto_library( deps = [":example_proto"], ) -proto_library( - name = "interaction_object_proto", - srcs = ["src/main/proto/interaction_object.proto"], +format_import_proto_library( + name = "interaction_object", + src = "src/main/proto/interaction_object.proto", + deps = [ + ":math_proto", + ], ) java_lite_proto_library( @@ -66,6 +69,16 @@ java_lite_proto_library( deps = [":interaction_object_proto"], ) +proto_library( + name = "math_proto", + srcs = ["src/main/proto/math.proto"], +) + +java_lite_proto_library( + name = "math_java_proto_lite", + deps = [":math_proto"], +) + proto_library( name = "onboarding_proto", srcs = ["src/main/proto/onboarding.proto"], @@ -191,6 +204,7 @@ android_library( ":event_logger_java_proto_lite", ":example_java_proto_lite", ":interaction_object_java_proto_lite", + ":math_java_proto_lite", ":onboarding_java_proto_lite", ":profile_java_proto_lite", ":subtitled_html_java_proto_lite", diff --git a/model/src/main/proto/format_import_proto_library.bzl b/model/src/main/proto/format_import_proto_library.bzl index 8e7aaf0e1a6..8f09cd22cf8 100644 --- a/model/src/main/proto/format_import_proto_library.bzl +++ b/model/src/main/proto/format_import_proto_library.bzl @@ -25,7 +25,8 @@ def format_import_proto_library(name, src, deps): sed 's/import \"/import \"model\/src\/main\/proto\//g' | sed 's/\"model\/src\/main\/proto\/exploration/\"model\/processed_src\/main\/proto\/exploration/g' | sed 's/\"model\/src\/main\/proto\/topic/\"model\/processed_src\/main\/proto\/topic/g' | - sed 's/\"model\/src\/main\/proto\/question/\"model\/processed_src\/main\/proto\/question/g' > $@ + sed 's/\"model\/src\/main\/proto\/question/\"model\/processed_src\/main\/proto\/question/g' | + sed 's/\"model\/src\/main\/proto\/interaction_object/\"model\/processed_src\/main\/proto\/interaction_object/g' > $@ ''', ) diff --git a/model/src/main/proto/interaction_object.proto b/model/src/main/proto/interaction_object.proto index 212f696ade9..5ff11c3d42c 100644 --- a/model/src/main/proto/interaction_object.proto +++ b/model/src/main/proto/interaction_object.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package model; +import "math.proto"; + option java_package = "org.oppia.android.app.model"; option java_multiple_files = true; @@ -53,14 +55,6 @@ message NumberUnit { int32 exponent = 2; } -// Structure for a fraction object. -message Fraction { - bool is_negative = 1; - int32 whole_number = 2; - int32 numerator = 3; - int32 denominator = 4; -} - // Structure for a ListOfString object. message ListOfSetsOfHtmlStrings { repeated StringList set_of_html_strings = 1; diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto new file mode 100644 index 00000000000..73f23bd8c01 --- /dev/null +++ b/model/src/main/proto/math.proto @@ -0,0 +1,79 @@ +syntax = "proto3"; + +package model; + +option java_package = "org.oppia.android.app.model"; +option java_multiple_files = true; + +// Structure for a fraction object. +message Fraction { + bool is_negative = 1; + int32 whole_number = 2; + int32 numerator = 3; + int32 denominator = 4; +} + +// Represents a mathematical expression such as 1+2. The only expression currently supported is a +// binary operation. +message MathExpression { + oneof expression_type { + Real constant = 1; + string variable = 2; + MathBinaryOperation binary_operation = 3; + MathUnaryOperation unary_operation = 4; + } +} + +message MathBinaryOperation { + enum Operator { + OPERATOR_UNKNOWN = 0; + // Represents adding two values, e.g.: 1 + x. + ADD = 1; + // Represents subtracting two values, e.g.: x - 2. + SUBTRACT = 2; + // Represents multiplying two values, e.g.: x * y. + MULTIPLY = 3; + // Represents dividing two values, e.g.: 1/x. + DIVIDE = 4; + // Represents taking the exponentiation of one value by another, e.g.: x^2. + EXPONENTIATE = 5; + } + + Operator operator = 1; + MathExpression left_operand = 2; + MathExpression right_operand = 3; +} + +message MathUnaryOperation { + enum Operator { + OPERATOR_UNKNOWN = 0; + // Represents negating a value, e.g.: -y. + NEGATION = 1; + } + + Operator operator = 1; + MathExpression operand = 2; +} + +message Real { + oneof real_type { + Fraction rational = 1; + // Represents a decimal value. Technically these can sometimes be rational, but given IEEE-754 + // rounding errors we need to treat these values as irrational and non-factorable. + double irrational = 2; + } +} + +message Polynomial { + repeated Term term = 1; + + message Term { + Real coefficient = 1; + repeated Variable variable = 2; + + message Variable { + string name = 1; + int32 power = 2; + } + } +} diff --git a/utility/BUILD.bazel b/utility/BUILD.bazel index 9c2695b618e..dc7960326ec 100644 --- a/utility/BUILD.bazel +++ b/utility/BUILD.bazel @@ -12,7 +12,10 @@ load("//utility:utility_test.bzl", "utility_test") # Library for general-purpose utilities. kt_android_library( name = "utility", - srcs = glob(["src/main/java/org/oppia/android/util/**/*.kt"]), + srcs = glob( + include = ["src/main/java/org/oppia/android/util/**/*.kt"], + exclude = ["src/main/java/org/oppia/android/util/math/**/*.kt"], + ), custom_package = "org.oppia.android.util", manifest = "src/main/AndroidManifest.xml", resource_files = glob(["src/main/res/**/*.xml"]), @@ -33,6 +36,23 @@ kt_android_library( ], ) +# Utilities specific to mathematics content. +kt_android_library( + name = "math", + srcs = [ + "src/main/java/org/oppia/android/util/math/FloatExtensions.kt", + "src/main/java/org/oppia/android/util/math/FractionExtensions.kt", + "src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt", + "src/main/java/org/oppia/android/util/math/MathExpressionParser.kt", + "src/main/java/org/oppia/android/util/math/MathTokenizer.kt", + ], + custom_package = "org.oppia.android.util.math", + visibility = ["//visibility:public"], + deps = [ + "//model", + ], +) + TEST_DEPS = [ ":utility", ":dagger", @@ -104,4 +124,22 @@ utility_test( deps = TEST_DEPS, ) +utility_test( + name = "MathTokenizerTest", + srcs = ["src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt"], + test_class = "org.oppia.android.util.math.MathTokenizerTest", + deps = TEST_DEPS + [ + ":math", + ], +) + +utility_test( + name = "MathExpressionParserTest", + srcs = ["src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt"], + test_class = "org.oppia.android.util.math.MathExpressionParserTest", + deps = TEST_DEPS + [ + ":math", + ], +) + dagger_rules() diff --git a/domain/src/main/java/org/oppia/android/domain/util/FloatExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt similarity index 93% rename from domain/src/main/java/org/oppia/android/domain/util/FloatExtensions.kt rename to utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt index 5ce8d4b0c10..8d309a67999 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/FloatExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt @@ -1,4 +1,4 @@ -package org.oppia.android.domain.util +package org.oppia.android.util.math import kotlin.math.abs diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt new file mode 100644 index 00000000000..751a2af42e6 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -0,0 +1,196 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.Fraction +import kotlin.math.absoluteValue + +/** + * Returns a submittable answer string representation of this fraction (note that this may not be + * the verbatim string originally submitted by the user, if any. + */ +fun Fraction.toAnswerString(): String { + return when { + isOnlyWholeNumber() -> { + // Fraction is only a whole number. + if (isNegative) "-$wholeNumber" else "$wholeNumber" + } + wholeNumber == 0 -> { + // Fraction contains just a fraction (no whole number). + when (denominator) { + 1 -> if (isNegative) "-$numerator" else "$numerator" + else -> if (isNegative) "-$numerator/$denominator" else "$numerator/$denominator" + } + } + else -> { + // Otherwise it's a mixed number. Note that the denominator is always shown here to account + // for strange cases that would require evaluation to resolve, such as: "2 2/1". + if (isNegative) { + "-$wholeNumber $numerator/$denominator" + } else { + "$wholeNumber $numerator/$denominator" + } + } + } +} + +/** Returns whether this fraction has a fractional component. */ +fun Fraction.hasFractionalPart(): Boolean { + return numerator != 0 +} + +/** + * Returns whether this fraction only represents a whole number. Note that for the fraction '0' this + * will return true. + */ +fun Fraction.isOnlyWholeNumber(): Boolean { + return !hasFractionalPart() +} + +/** + * Returns a float version of this fraction. + * + * See: https://github.com/oppia/oppia/blob/37285a/core/templates/dev/head/domain/objects/FractionObjectFactory.ts#L73. + */ +fun Fraction.toFloat(): Float { + val totalParts = ((wholeNumber * denominator) + numerator).toFloat() + val floatVal = totalParts / denominator.toFloat() + return if (isNegative) -floatVal else floatVal +} + +/** + * Double version of [toFloat] (note that this doesn't actually guarantee additional precision over + * toFloat(). + */ +fun Fraction.toDouble(): Double = toFloat().toDouble() + +/** + * Returns this fraction in its most simplified form. + * + * See: https://github.com/oppia/oppia/blob/37285a/core/templates/dev/head/domain/objects/FractionObjectFactory.ts#L83. + */ +fun Fraction.toSimplestForm(): Fraction { + val commonDenominator = gcd(numerator, denominator) + return toBuilder() + .setWholeNumber(wholeNumber) + .setNumerator(numerator / commonDenominator) + .setDenominator(denominator / commonDenominator) + .build() +} + +/** + * Returns this fraction in an improper form (that is, with a 0 whole number and only fractional + * parts). + */ +fun Fraction.toImproperForm(): Fraction { + return toBuilder().setNumerator(numerator + (denominator * wholeNumber)).setWholeNumber(0).build() +} + +/** + * Returns this fraction in its proper form by first converting to simplest denominator, then + * extracting a whole number component. + * + * This function will properly convert a fraction whose denominator is 1 into a whole number-only + * fraction. + */ +fun Fraction.toProperForm(): Fraction { + return toSimplestForm().let { + it.toBuilder() + .setWholeNumber(it.wholeNumber + (it.numerator / it.denominator)) + .setNumerator(it.numerator % it.denominator) + .build() + } +} + +/** Adds two fractions together and returns a new one in its proper form. */ +operator fun Fraction.plus(rhs: Fraction): Fraction { + // First, eliminate the whole number by computing improper fractions. + val leftFraction = toImproperForm() + val rightFraction = rhs.toImproperForm() + + // Second, find a common denominator and compute the new numerators. + val commonDenominator = lcm(leftFraction.denominator, rightFraction.denominator) + val leftFactor = commonDenominator / leftFraction.denominator + val rightFactor = commonDenominator / rightFraction.denominator + val leftNumerator = leftFraction.numerator * leftFactor + val rightNumerator = rightFraction.numerator * rightFactor + + // Third, determine how the numerators are combined (based on negatives) and whether the result is + // negative. + val leftNeg = leftFraction.isNegative + val rightNeg = rightFraction.isNegative + val (newNumerator, isNegative) = when { + leftNeg && rightNeg -> leftNumerator + rightNumerator to true + !leftNeg && !rightNeg -> leftNumerator + rightNumerator to false + leftNeg && !rightNeg -> + (-leftNumerator + rightNumerator).absoluteValue to (leftNumerator > rightNumerator) + !leftNeg && rightNeg -> + (leftNumerator - rightNumerator).absoluteValue to (rightNumerator > leftNumerator) + else -> throw Exception("Impossible case") + } + + // Finally, compute the new fraction and convert it to proper form to compute its whole number. + return Fraction.newBuilder() + .setIsNegative(isNegative) + .setNumerator(newNumerator) + .setDenominator(commonDenominator) + .build() + .toProperForm() +} + +/** + * Subtracts the specified fraction from this fraction and returns the result in its proper form. + */ +operator fun Fraction.minus(rhs: Fraction): Fraction { + // a - b = a + -b + return this + -rhs +} + +/** Multiples this fraction by the specified and returns the result in its proper form. */ +operator fun Fraction.times(rhs: Fraction): Fraction { + // First, convert both fractions into their improper forms. + val leftFraction = toImproperForm() + val rightFraction = rhs.toImproperForm() + + // Second, multiple the numerators and denominators piece-wise. + val newNumerator = leftFraction.numerator * rightFraction.numerator + val newDenominator = leftFraction.denominator * rightFraction.denominator + + // Third, determine negative (negative is retained if only one is negative). + val isNegative = leftFraction.isNegative xor rightFraction.isNegative + return Fraction.newBuilder() + .setIsNegative(isNegative) + .setNumerator(newNumerator) + .setDenominator(newDenominator) + .build() + .toProperForm() +} + +/** Returns the proper form of the division from this fraction by the specified fraction. */ +operator fun Fraction.div(rhs: Fraction): Fraction { + // a / b = a * b^-1 (b's inverse). + return this * rhs.toInvertedImproperForm() +} + +/** Returns the inverse improper fraction representation of this fraction. */ +private fun Fraction.toInvertedImproperForm(): Fraction { + val improper = toImproperForm() + return improper.toBuilder() + .setNumerator(improper.denominator) + .setDenominator(improper.numerator) + .build() +} + +/** Returns the negated form of this fraction. */ +operator fun Fraction.unaryMinus(): Fraction { + return toBuilder().setIsNegative(!isNegative).build() +} + +/** Returns the greatest common divisor between two integers. */ +fun gcd(x: Int, y: Int): Int { + return if (y == 0) x else gcd(y, x % y) +} + +/** Returns the least common multiple between two integers. */ +fun lcm(x: Int, y: Int): Int { + // Reference: https://en.wikipedia.org/wiki/Least_common_multiple#Calculation. + return (x * y).absoluteValue / gcd(x, y) +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt new file mode 100644 index 00000000000..accc3c6660f --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -0,0 +1,544 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.Fraction +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.Polynomial +import org.oppia.android.app.model.Polynomial.Term +import org.oppia.android.app.model.Polynomial.Term.Variable +import org.oppia.android.app.model.Real +import kotlin.math.pow + +// TODO: split up this extensions file into multiple, clean it up, reorganize, and add tests. + +// XXX: Polynomials can actually take lots of forms, so this conversion is intentionally limited +// based on intended use cases and can be extended in the future, as needed. For example, both +// x^(2^3) and ((x^2+2x+1)/(x+1)) won't be considered polynomials, but it can be seen that they are +// indeed after being simplified. This implementation doesn't yet support recognizing binomials +// (e.g. (x+1)^2) but needs to be updated to. The representation for Polynomial doesn't quite +// support much outside of general form. + +// Consider expressions like x/2 (should be treated like (1/2)x). +// Consider: (1+2)*x or (1+2)x + +// Also: consider how to perform expression analysis for non-polynomial trees + +fun MathExpression.toPolynomial(): Polynomial? { + // Constructing a polynomial more or less requires: + // 1. Collecting all variables (these will either be part of exponent expressions if they have a + // power, part of a multiplication expression with a constant/evaluable constant directly or once + // removed, or part of another expression) and evaluating them into terms. + // 2. Collecting all non-variable values as top-level constant terms. + // Note that polynomials are always representable via summation, so multiplication, division, and + // subtraction must be partially evaluated and eliminated (if they can't be eliminated then the + // expression is not polynomial or requires a more complex algorithm for reduction such as + // polynomial long division). + + // ----- Algo probably requires additional data structure component since it's changing the tree piece-by-piece. + // Consider having two versions of the conversion: one for rigid polynomials and another for forcing expressions into a polynomial. + + // First thoughts on a possible algorithm: + // 1. Find all variable expressions, then go up 1 node (short-circuit: if no parent, it's a polynomial with 1 variable term) + // 2. For each variable: + // a. If the parent is a multiplication expression, try to reduce the other term to a constant (this would be the term of the variable). Remove the multiplication term. + // b. If the parent is an exponent, try to reduce the right-hand side. This would become the variable's power. Remove the exponent term. + // c. If the parent is a unary operation, in-line that. + // 3. Repeat (2) until variables become irreducible. + // 4. Enumerate all exponents, multiplications, and divisions: reduce each at its parent to a constant. + // 5. Replace remaining subtractions with additions + unary operators, then reduce all unary operations to constants. + // 6. Check: there should be no remaining exponents, multiplications, divisions, subtractions, or unary operations (only addition should remain). If there are, fail: this isn't a polynomial or it isn't one that we support. + // 7. Traverse the tree and convert each addition operand into a term and construct the final polynomial. + // 8. Optional: further reduce the polynomial and/or convert to general form. + + // Consider revising the above to recursively find nested polynomials and "build them up". This + // will allow us to detect each of the pathological cases that can't be handled by the above, plus + // trivial cases the are handled by the above: + // 1. Top-level polynomial / polynomials being added / polynomials being subtracted (should be handled by the above algo) + // 2. Polynomial being divided by another polynomial (we should just fail here--it's quite complex to solve this) + // 3. Polynomial raised by a constant positive whole number power (e.g. binomial); any other exponent isn't a polynomial (including a polynomial exp) + // 4. Polynomials multiplied by each other (requires expanding like #3, probably via matrix multiplication + // 5. Combinations of 1-4 (which requires recursion to find a complete solution for) + + // Final algorithm (non-simplified): + // 1. Copy the tree so it can be augmented as nodes(expression | polynomial | constant) + // 2. Replace all variable expressions with polynomials + // 3. Depth-first evaluate the entire graph (results are polynomials OR concatenation). If any fails, this is not a polynomial or is unsupported. Specifics: + // a. Unary (apply to coefficients of the terms) + // b. Exponents (right-hand side must be reducible to constant -> calculate the power); may require expansion (e.g. for binomials) + // c. Subtraction (replace with addition & negate the coefficient of the polynomial terms) + // d. Division (right-hand side must be reducible to constant -> apply to the term, e.g. for x/4) + // e. Multiplication (for one side constant, apply to coefficients otherwise perform polynomial multiplication) + // f. Addition (treat constants as constant terms & concatenate term lists to compute new polynomial) + // 4. Collect the final polynomial as the result. Early exiting indicates a non-polynomial. + return reduceToPolynomial() +} + +fun Polynomial.isUnivariate(): Boolean = getUniqueVariableCount() == 1 + +fun Polynomial.isMultivariate(): Boolean = getUniqueVariableCount() > 1 + +private fun Polynomial.getUniqueVariableCount(): Int { + return termList.flatMap(Term::getVariableList).map(Variable::getName).toSet().size +} + +fun Polynomial.toAnswerString(): String { + return termList.joinToString(separator = " + ", transform = Term::toAnswerString) +} + +private fun Term.toAnswerString(): String { + val productValues = mutableListOf() + + // Include the coefficient if there is one (coefficients of 1 are ignored only if there are + // variables present). + if (!coefficient.isApproximatelyEqualTo(1.0) || variableList.isEmpty()) { + productValues += coefficient.toAnswerString() + } + + // Include any present variables. + productValues += variableList.map(Variable::toAnswerString) + + // Take the product of all relevant values of the term. + return productValues.joinToString(separator = "*") +} + +private fun Variable.toAnswerString(): String { + return if (power > 1) "$name^$power" else name +} + +private fun Real.toAnswerString(): String { + // Note that the rational part is first converted to an improper fraction since mixed fractions + // can't be expressed as a single coefficient in typical polynomial syntax). + return if (hasRational()) rational.toImproperForm().toAnswerString() else irrational.toString() +} + +private fun MathExpression.reduceToPolynomial(): Polynomial? { + return when (expressionTypeCase) { + CONSTANT -> createPolynomialFromConstant(constant) + VARIABLE -> createSingleTermPolynomial(variable) + UNARY_OPERATION -> unaryOperation.reduceToPolynomial() + BINARY_OPERATION -> binaryOperation.reduceToPolynomial() + else -> null + } +} + +private fun MathUnaryOperation.reduceToPolynomial(): Polynomial? { + return when (operator) { + MathUnaryOperation.Operator.NEGATION -> -(operand.reduceToPolynomial() ?: return null) + else -> null + } +} + +private fun MathBinaryOperation.reduceToPolynomial(): Polynomial? { + val leftPolynomial = leftOperand.reduceToPolynomial() ?: return null + val rightPolynomial = rightOperand.reduceToPolynomial() ?: return null + return when (operator) { + MathBinaryOperation.Operator.ADD -> leftPolynomial + rightPolynomial + MathBinaryOperation.Operator.SUBTRACT -> leftPolynomial - rightPolynomial + MathBinaryOperation.Operator.MULTIPLY -> leftPolynomial * rightPolynomial + MathBinaryOperation.Operator.DIVIDE -> leftPolynomial / rightPolynomial + MathBinaryOperation.Operator.EXPONENTIATE -> leftPolynomial.pow(rightPolynomial) + else -> null + } +} + +/** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ +private fun Polynomial.isConstant(): Boolean { + return termCount == 1 && getTerm(0).variableCount == 0 +} + +/** + * Returns the first term coefficient from this polynomial. This corresponds to the whole value of + * the polynomial iff isConstant() returns true, otherwise this value isn't useful. + * + * Note that this function can throw if the polynomial is empty (so isConstant() should always be + * checked first). + */ +private fun Polynomial.getConstant(): Real { + return getTerm(0).coefficient +} + +private operator fun Polynomial.unaryMinus(): Polynomial { + // Negating a polynomial just requires flipping the signs on all coefficients. + return toBuilder() + .clearTerm() + .addAllTerm(termList.map { it.toBuilder().setCoefficient(-it.coefficient).build() }) + .build() +} + +private operator fun Polynomial.plus(rhs: Polynomial): Polynomial { + // Adding two polynomials just requires combining their terms lists. + return Polynomial.newBuilder().addAllTerm(termList).addAllTerm(rhs.termList).build() +} + +private operator fun Polynomial.minus(rhs: Polynomial): Polynomial { + // a - b = a + -b + return this + -rhs +} + +private operator fun Polynomial.times(rhs: Polynomial): Polynomial { + // Polynomial multiplication is simply multiplying each term in one by each term in the other. + // TODO: ensure this properly computes trivial cases like (x^2 becoming x-squared) or whether + // those cases need to be special cased. + return Polynomial.newBuilder() + .addAllTerm(termList.flatMap { leftTerm -> + rhs.termList.map { rightTerm -> leftTerm * rightTerm } + }).build() +} + +private operator fun Term.times(rhs: Term): Term { + // The coefficients are always multiplied. + val combinedCoefficient = coefficient * rhs.coefficient + + // Next, create a combined list of new variables. + val combinedVariables = variableList + rhs.variableList + + // Simplify the variables by combining the exponents of like variables. Start with a map of 0 + // powers, then add in the powers of each variable and collect the final list of unique terms. + val variableNamesMap = mutableMapOf() + combinedVariables.forEach { + variableNamesMap.compute(it.name) { _, power -> + if (power != null) power + it.power else it.power + } + } + val newVariableList = variableNamesMap.map { (name, power) -> + Variable.newBuilder().setName(name).setPower(power).build() + } + + return Term.newBuilder() + .setCoefficient(combinedCoefficient) + .addAllVariable(newVariableList) + .build() +} + +private operator fun Polynomial.div(rhs: Polynomial): Polynomial? { + // TODO: ensure this properly computes distributions for fractions, e.g. ((x+3)/2) should become + // (1/2)x + (3/2). + // See https://en.wikipedia.org/wiki/Polynomial_long_division#Pseudocode for reference. + if (rhs.isApproximatelyZero()) { + // TODO: test (x+2)/0 + return null // Dividing by zero is invalid and thus cannot yield a polynomial. + } + + var quotient = createPolynomialFromConstant(createCoefficientValueOf(value = 0)) + var remainder = this + val divisorDegree = rhs.getDegree() + val leadingDivisorTerm = rhs.getLeadingTerm() + while (!remainder.isApproximatelyZero() && remainder.getDegree() >= divisorDegree) { + // Attempt to divide the leading terms (this may fail). + val newTerm = remainder.getLeadingTerm() / leadingDivisorTerm ?: return null + quotient += newTerm.toPolynomial() + remainder -= newTerm.toPolynomial() * rhs + } + if (!remainder.isApproximatelyZero()) { + // A non-zero remainder indicates the division was not "pure" which means the result is a + // non-polynomial. + return null + } + return quotient +} + +private fun Term.toPolynomial(): Polynomial { + return Polynomial.newBuilder().addTerm(this).build() +} + +private operator fun Term.div(rhs: Term): Term? { + val dividendPowerMap = variableList.toPowerMap() + val divisorPowerMap = rhs.variableList.toPowerMap() + + // If any variables are present in the divisor and not the dividend, this division won't work + // effectively. + if (!dividendPowerMap.keys.containsAll(divisorPowerMap.keys)) return null + + // Division is simply subtracting the powers of terms in the divisor from those in the dividend. + val quotientPowerMap = dividendPowerMap.mapValues { (name, power) -> + power - divisorPowerMap.getOrDefault(name, defaultValue = 0) + } + + // If there are any negative powers, the divisor can't effectively divide this value. + if (quotientPowerMap.values.any { it < 0 }) return null + + // Remove variables with powers of 0 since those have been fully divided. Also, divide the + // coefficients to finish the division. + return Term.newBuilder() + .setCoefficient(coefficient / rhs.coefficient) + .addAllVariable(quotientPowerMap.filter { (_, power) -> power > 0 }.toVariableList()) + .build() +} + +private fun List.toPowerMap(): Map { + return associateBy({ it.name }, { it.power }) +} + +private fun Map.toVariableList(): List { + return map { (name, power) -> Variable.newBuilder().setName(name).setPower(power).build() } +} + +private fun Polynomial.getLeadingTerm(): Term { + // Return the leading term. Reference: https://undergroundmathematics.org/glossary/leading-term. + return termList.reduce { maxTerm, term -> + val maxTermDegree = maxTerm.highestDegree() + val termDegree = term.highestDegree() + return@reduce if (termDegree > maxTermDegree) term else maxTerm + } +} + +// Return the highest power to represent the degree of the polynomial. Reference: +// https://www.varsitytutors.com/algebra_1-help/how-to-find-the-degree-of-a-polynomial. +private fun Polynomial.getDegree(): Int = getLeadingTerm().highestDegree() + +private fun Term.highestDegree(): Int { + return variableList.map(Variable::getPower).max() ?: 0 +} + +private fun Polynomial.isApproximatelyZero(): Boolean { + return isConstant() && getConstant().isApproximatelyZero() +} + +private fun Polynomial.pow(exp: Int): Polynomial { + // Anything raised to the power of 0 is 1. + if (exp == 0) return createPolynomialFromConstant(createCoefficientValueOfOne()) + if (exp == 1) return this + var newValue = this + for (i in 1 until exp) newValue *= this + return newValue +} + +private fun Polynomial.pow(exp: Real): Polynomial? { + // Polynomials can only be raised to positive integers (or zero). + return if (exp.hasRational() && exp.rational.isOnlyWholeNumber() && !exp.rational.isNegative) { + pow(exp.rational.wholeNumber) + } else null +} + +private fun Polynomial.pow(exp: Polynomial): Polynomial? { + // Polynomial exponentiation is only supported if the right side is a constant polynomial, + // otherwise the result cannot be a polynomial. + return if (exp.isConstant()) pow(exp.getConstant()) else null +} + +private fun MathExpression.toTreeNode(): ExpressionTreeNode { + return when (expressionTypeCase) { + CONSTANT -> ExpressionTreeNode.ConstantNode(constant) + VARIABLE -> ExpressionTreeNode.PolynomialNode(createSingleTermPolynomial(variable)) + UNARY_OPERATION -> ExpressionTreeNode.ExpressionNode(this, unaryOperation.collectChildren()) + BINARY_OPERATION -> ExpressionTreeNode.ExpressionNode(this, binaryOperation.collectChildren()) + else -> ExpressionTreeNode.ExpressionNode(this, mutableListOf()) + } +} + +private fun MathUnaryOperation.collectChildren(): MutableList { + return mutableListOf(operand.toTreeNode()) +} + +private fun MathBinaryOperation.collectChildren(): MutableList { + return mutableListOf(leftOperand.toTreeNode(), rightOperand.toTreeNode()) +} + +private fun createSingleTermPolynomial(variableName: String): Polynomial { + return Polynomial.newBuilder() + .addTerm( + Term.newBuilder() + .setCoefficient(createCoefficientValueOfOne()) + .addVariable(Variable.newBuilder().setName(variableName).setPower(1)) + ).build() +} + +private fun createPolynomialFromConstant(constant: Real): Polynomial { + return Polynomial.newBuilder() + .addTerm(Term.newBuilder().setCoefficient(constant)) + .build() +} + +private fun createCoefficientValueOf(value: Int): Real { + return Real.newBuilder() + .setRational(Fraction.newBuilder().setWholeNumber(value).setDenominator(1)) + .build() +} + +private fun createCoefficientValueOfOne(): Real = createCoefficientValueOf(value = 1) + +private sealed class ExpressionTreeNode { + data class ExpressionNode( + val mathExpression: MathExpression, + val children: MutableList + ): ExpressionTreeNode() + + data class PolynomialNode(val polynomial: Polynomial): ExpressionTreeNode() + + data class ConstantNode(val constant: Real): ExpressionTreeNode() +} + +// TODO: add a faster isReducibleToConstant recursive function since this is used a lot. + +//private fun MathExpression.reduceToConstant(): MathExpression? { +// return when (expressionTypeCase) { +// CONSTANT -> this +// VARIABLE -> null +// UNARY_OPERATION -> unaryOperation.reduceToConstant() +// BINARY_OPERATION -> binaryOperation.reduceToConstant() +// else -> null +// } +//} + +//private fun MathUnaryOperation.reduceToConstant(): MathExpression? { +// return when (operator) { +// MathUnaryOperation.Operator.NEGATION -> operand.reduceToConstant()?.transformConstant { -it } +// else -> null +// } +//} + +//private fun MathBinaryOperation.reduceToConstant(): MathExpression? { +// val leftConstant = leftOperand.reduceToConstant()?.constant ?: return null +// val rightConstant = rightOperand.reduceToConstant()?.constant ?: return null +// return when (operator) { +// MathBinaryOperation.Operator.ADD -> fromConstant(leftConstant + rightConstant) +// MathBinaryOperation.Operator.SUBTRACT -> fromConstant(leftConstant - rightConstant) +// MathBinaryOperation.Operator.MULTIPLY -> fromConstant(leftConstant * rightConstant) +// MathBinaryOperation.Operator.DIVIDE -> fromConstant(leftConstant / rightConstant) +// MathBinaryOperation.Operator.EXPONENTIATE -> fromConstant(leftConstant.pow(rightConstant)) +// else -> null +// } +//} + +private fun MathExpression.transformConstant( + transform: (Real.Builder) -> Real.Builder +): MathExpression { + return toBuilder().setConstant(transform(constant.toBuilder())).build() +} + +private fun fromConstant(real: Real): MathExpression { + return MathExpression.newBuilder().setConstant(real).build() +} + +private fun Real.isApproximatelyEqualTo(value: Double): Boolean { + return toDouble().approximatelyEquals(value) +} + +private fun Real.isApproximatelyZero(): Boolean = isApproximatelyEqualTo(0.0) + +private fun Real.toDouble(): Double { + return if (hasRational()) rational.toDouble() else irrational +} + +private fun Real.recompute(transform: (Real.Builder) -> Real.Builder): Real { + return transform(toBuilder().clearRational().clearIrrational()).build() +} + +private fun combine( + lhs: Real, + rhs: Real, + leftRationalRightRationalOp: (Fraction, Fraction) -> Fraction, + leftRationalRightIrrationalOp: (Fraction, Double) -> Double, + leftIrrationalRightRationalOp: (Double, Fraction) -> Double, + leftIrrationalRightIrrationalOp: (Double, Double) -> Double): Real { + return when (lhs.realTypeCase) { + Real.RealTypeCase.RATIONAL -> { + // Left-hand side is Fraction. + when (rhs.realTypeCase) { + Real.RealTypeCase.RATIONAL -> + lhs.recompute { it.setRational(leftRationalRightRationalOp(lhs.rational, rhs.rational)) } + Real.RealTypeCase.IRRATIONAL -> + lhs.recompute { + it.setIrrational(leftRationalRightIrrationalOp(lhs.rational, rhs.irrational)) + } + else -> throw Exception("Invalid real: $rhs.") + } + } + Real.RealTypeCase.IRRATIONAL -> { + // Left-hand side is a double. + when (rhs.realTypeCase) { + Real.RealTypeCase.RATIONAL -> + lhs.recompute { + it.setIrrational(leftIrrationalRightRationalOp(lhs.irrational, rhs.rational)) + } + Real.RealTypeCase.IRRATIONAL -> + lhs.recompute { + it.setIrrational(leftIrrationalRightIrrationalOp(lhs.irrational, rhs.irrational)) + } + else -> throw Exception("Invalid real: $rhs.") + } + } + else -> throw Exception("Invalid real: $lhs.") + } +} + +private fun Real.pow(rhs: Real): Real { + // Powers can really only be effectively done via floats or whole-number only fractions. + return when (realTypeCase) { + Real.RealTypeCase.RATIONAL -> { + // Left-hand side is Fraction. + when (rhs.realTypeCase) { + Real.RealTypeCase.RATIONAL -> recompute { + if (rhs.rational.isOnlyWholeNumber()) { + // The fraction can be retained. + it.setRational(rational.pow(rhs.rational.wholeNumber)) + } else { + // The fraction can't realistically be retained since it's being raised to an actual + // fraction, resulting in an irrational number. + it.setIrrational(rational.toDouble().pow(rhs.rational.toDouble())) + } + } + Real.RealTypeCase.IRRATIONAL -> recompute { it.setIrrational(rational.pow(rhs.irrational)) } + else -> throw Exception("Invalid real: $rhs.") + } + } + Real.RealTypeCase.IRRATIONAL -> { + // Left-hand side is a double. + when (rhs.realTypeCase) { + Real.RealTypeCase.RATIONAL -> recompute { it.setIrrational(irrational.pow(rhs.rational)) } + Real.RealTypeCase.IRRATIONAL -> + recompute { it.setIrrational(irrational.pow(rhs.irrational)) } + else -> throw Exception("Invalid real: $rhs.") + } + } + else -> throw Exception("Invalid real: $this.") + } +} + +private operator fun Real.unaryMinus(): Real { + return when (realTypeCase) { + Real.RealTypeCase.RATIONAL -> recompute { it.setRational(-rational) } + Real.RealTypeCase.IRRATIONAL -> recompute { it.setIrrational(-irrational) } + else -> throw Exception("Invalid real: $this.") + } +} + +private operator fun Real.plus(rhs: Real): Real { + return combine(this, rhs, Fraction::plus, Fraction::plus, Double::plus, Double::plus) +} + +private operator fun Real.minus(rhs: Real): Real { + return combine(this, rhs, Fraction::minus, Fraction::minus, Double::minus, Double::minus) +} + +private operator fun Real.times(rhs: Real): Real { + return combine(this, rhs, Fraction::times, Fraction::times, Double::times, Double::times) +} + +private operator fun Real.div(rhs: Real): Real { + return combine(this, rhs, Fraction::div, Fraction::div, Double::div, Double::div) +} + +private fun Double.pow(rhs: Fraction): Double = this.pow(rhs.toDouble()) +private fun Fraction.pow(rhs: Double): Double = toDouble().pow(rhs) +private operator fun Double.plus(rhs: Fraction): Double = this + rhs.toFloat() +private operator fun Fraction.plus(rhs: Double): Double = toFloat() + rhs +private operator fun Double.minus(rhs: Fraction): Double = this - rhs.toFloat() +private operator fun Fraction.minus(rhs: Double): Double = toFloat() - rhs +private operator fun Double.times(rhs: Fraction): Double = this * rhs.toFloat() +private operator fun Fraction.times(rhs: Double): Double = toFloat() * rhs +private operator fun Double.div(rhs: Fraction): Double = this / rhs.toFloat() +private operator fun Fraction.div(rhs: Double): Double = toFloat() / rhs + +private fun Fraction.pow(exp: Int): Fraction { + if (exp == 0) return Fraction.newBuilder().setWholeNumber(1).setDenominator(1).build() + if (exp == 1) return this + var newValue = this + for (i in 1 until exp) newValue *= this + return newValue +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt new file mode 100644 index 00000000000..193d423e93b --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -0,0 +1,236 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.Fraction +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.Real +import org.oppia.android.util.math.MathTokenizer.Token.CloseParenthesis +import org.oppia.android.util.math.MathTokenizer.Token.DecimalNumber +import org.oppia.android.util.math.MathTokenizer.Token.Identifier +import org.oppia.android.util.math.MathTokenizer.Token.InvalidToken +import org.oppia.android.util.math.MathTokenizer.Token.OpenParenthesis +import org.oppia.android.util.math.MathTokenizer.Token.Operator +import org.oppia.android.util.math.MathTokenizer.Token.WholeNumber +import java.util.ArrayDeque +import java.util.Stack + +private val OPERATOR_PRECEDENCES = mapOf('^' to 4, '*' to 3, '/' to 3, '+' to 2, '-' to 2) +private val LEFT_ASSOCIATIVE_OPERATORS = listOf('*', '/', '+', '-') + +class MathExpressionParser { + companion object { + sealed class ParseResult { + data class Success(val mathExpression: MathExpression) : ParseResult() + + data class Failure(val failureReason: String) : ParseResult() + } + + // TODO: update to support implied multiplication, e.g.: 2x^2. + fun parseExpression(literalExpression: String): ParseResult { + // An implementation of the Shunting Yard algorithm adapted to support variables, different + // number types, and the unary negation operator. References: + // - https://en.wikipedia.org/wiki/Shunting-yard_algorithm#The_algorithm_in_detail + // - https://wcipeg.com/wiki/Shunting_yard_algorithm#Unary_operators + val operatorStack = Stack() + val outputQueue = ArrayDeque() + var lastToken: MathTokenizer.Token? = null + for (token in MathTokenizer.tokenize(literalExpression)) { + when (token) { + is WholeNumber, is DecimalNumber, is Identifier -> { + outputQueue += ParsedToken(token) + } + is Operator -> { + val precedence = token.getPrecedence() + ?: return ParseResult.Failure("Encountered unexpected operator: ${token.operator}") + while (operatorStack.isNotEmpty()) { + val top = operatorStack.peek() + if (top.token !is Operator) break + val topPrecedence = top.token.getPrecedence() + ?: return ParseResult.Failure( + "Encountered unexpected operator: ${top.token.operator}" + ) + if (topPrecedence < precedence) break + if (topPrecedence == precedence && !token.isLeftAssociative()) break + outputQueue += operatorStack.pop() + } + // TODO: fix unary. + if (token.isMinusOperator() && lastToken.doesPreviousTokenIndicateNegation()) { + operatorStack.push(ParsedToken(token, isUnary = true)) + } else { + operatorStack.push(ParsedToken(token)) + } + } + is OpenParenthesis -> { + operatorStack.push(ParsedToken(token)) + } + is CloseParenthesis -> { + while (operatorStack.isNotEmpty()) { + val top = operatorStack.peek() + if (top.token is OpenParenthesis) break + outputQueue += operatorStack.pop() + } + if (operatorStack.isEmpty() || operatorStack.peek().token !is OpenParenthesis) { + return ParseResult.Failure( + "Encountered unexpected close parenthesis at index ${token.column} in " + + token.source + ) + } + // Discard the open parenthesis since it's be finished. + operatorStack.pop() + } + is InvalidToken -> { + return ParseResult.Failure( + "Encountered unexpected symbol at index ${token.column} in ${token.source}" + ) + } + } + lastToken = token + } + + while (operatorStack.isNotEmpty()) { + when (val top = operatorStack.peek().token) { + is OpenParenthesis -> return ParseResult.Failure( + "Encountered unexpected close parenthesis at index ${top.column} in ${top.source}" + ) + else -> outputQueue += operatorStack.pop() + } + } + + // We could alternatively reverse the token stream above & parse prefix notation immediately + // to avoid a second pass over the tokens (since then the expressions could be created + // in-line). However, two passes is simpler (and by using postfix notation we can avoid + // processing tokens that aren't needed if an error occurs during parsing). + val operandStack = Stack() + for (parsedToken in outputQueue) { + when (parsedToken.token) { + is WholeNumber, is DecimalNumber, is Identifier -> { + operandStack.push(TokenOrExpression.TokenWrapper(parsedToken.token)) + } + is Operator -> { + if (parsedToken.isUnary) { + if (parsedToken.token.operator != '-') { + return ParseResult.Failure( + "Encountered unexpected non-negation unary operator: " + + parsedToken.token.operator + ) + } + val unaryOperationExpression = + MathExpression.newBuilder() + .setUnaryOperation( + MathUnaryOperation.newBuilder() + .setOperator(MathUnaryOperation.Operator.NEGATION) + .assignOperand(operandStack.pop(), MathUnaryOperation.Builder::setOperand) + ).build() + operandStack.push(TokenOrExpression.ExpressionWrapper(unaryOperationExpression)) + } else { + val rightOperand = operandStack.pop() + val leftOperand = operandStack.pop() + val operator = parseBinaryOperator(parsedToken.token) + ?: return ParseResult.Failure( + "Encountered unexpected binary operator: ${parsedToken.token.operator}" + ) + val binaryOperationExpression = + MathExpression.newBuilder() + .setBinaryOperation( + MathBinaryOperation.newBuilder() + .setOperator(operator) + .assignOperand(leftOperand, MathBinaryOperation.Builder::setLeftOperand) + .assignOperand(rightOperand, MathBinaryOperation.Builder::setRightOperand) + ).build() + operandStack.push(TokenOrExpression.ExpressionWrapper(binaryOperationExpression)) + } + } + else -> + return ParseResult.Failure( + "Encountered unexpected token during parsing: ${parsedToken.token}" + ) + } + } + + val finalElement = operandStack.getFinalElement() + ?: return ParseResult.Failure("Failed to resolve expression tree: $operandStack") + return ParseResult.Success(finalElement.expression) + } + + /** + * Returns the final element of the stack (which should only contain that element) which itself + * should be an expression, or null if something failed during parsing. + */ + private fun Stack.getFinalElement(): TokenOrExpression.ExpressionWrapper? { + return if (size != 1 || firstElement() !is TokenOrExpression.ExpressionWrapper) null + else firstElement() as TokenOrExpression.ExpressionWrapper + } + + private fun B.assignOperand( + operand: TokenOrExpression, + setter: B.(MathExpression) -> B + ): B { + return when (operand) { + is TokenOrExpression.TokenWrapper -> this.setter(computeConstantOperand(operand.token)) + is TokenOrExpression.ExpressionWrapper -> this.setter(operand.expression) + } + } + + private fun parseBinaryOperator(operator: Operator): MathBinaryOperation.Operator? { + return when (operator.operator) { + '+' -> MathBinaryOperation.Operator.ADD + '-' -> MathBinaryOperation.Operator.SUBTRACT + '*' -> MathBinaryOperation.Operator.MULTIPLY + '/' -> MathBinaryOperation.Operator.DIVIDE + '^' -> MathBinaryOperation.Operator.EXPONENTIATE + else -> null + } + } + + private fun computeConstantOperand(token: MathTokenizer.Token): MathExpression { + return when (token) { + is WholeNumber -> + MathExpression.newBuilder() + .setConstant( + Real.newBuilder().setRational( + Fraction.newBuilder().setWholeNumber(token.value).setDenominator(1) + ) + ).build() + is DecimalNumber -> + MathExpression.newBuilder() + .setConstant(Real.newBuilder().setIrrational(token.value)) + .build() + is Identifier -> MathExpression.newBuilder().setVariable(token.name).build() + else -> MathExpression.getDefaultInstance() // This case should never happen. + } + } + + private sealed class TokenOrExpression { + + data class TokenWrapper(val token: MathTokenizer.Token) : TokenOrExpression() + + data class ExpressionWrapper(val expression: MathExpression) : TokenOrExpression() + } + + private data class ParsedToken(val token: MathTokenizer.Token, val isUnary: Boolean = false) + + /** + * Returns whether this token, as a previous token (potentially null for the first token of the + * stream) indicates that the token immediately following it could be a unary negation operator + * (if it's the minus operator). + */ + private fun MathTokenizer.Token?.doesPreviousTokenIndicateNegation(): Boolean { + // A minus operator at the beginning of the stream, after a group is opened, and after + // another operator is always a unary negation operator. + return this == null || this is OpenParenthesis || this is Operator + } + + private fun Operator.isMinusOperator(): Boolean { + return operator == '-' + } + + private fun Operator.getPrecedence(): Int? { + return OPERATOR_PRECEDENCES[operator] + } + + private fun Operator.isLeftAssociative(): Boolean { + return operator in LEFT_ASSOCIATIVE_OPERATORS + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt new file mode 100644 index 00000000000..cce210cfd5c --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt @@ -0,0 +1,308 @@ +package org.oppia.android.util.math + +import java.lang.IllegalStateException +import java.util.Locale + +// Only consider standard horizontal/vertical whitespace. +private val VALID_WHITESPACE = listOf(' ', '\t', '\n', '\r') +private val VALID_OPERATORS = listOf('*', '-', '+', '/', '^') +private val VALID_DIGITS = listOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9') +private val VALID_IDENTIFIER_LETTERS = "abcdefghijklmnopqrstuvwxyz".toCharArray().toList() + +/** + * Container class for functionality corresponding to tokenization of mathematics expressions. + * + * The current tokenization only supports basic polynomials. + */ +class MathTokenizer { + /** Corresponds to a token that may be found in a tokenized math expression. */ + sealed class Token { + abstract val source: String + abstract val column: Int + + /** Returns a human-readable string for this token. */ + abstract fun toReadableString(): String + + /** Corresponds to a valid operator: [*+-/^] */ + data class Operator( + override val source: String, + override val column: Int, + val operator: Char + ) : Token() { + override fun toReadableString(): String = operator.toString() + } + + /** Corresponds to a whole, non-decimal number: [0-9]+ */ + data class WholeNumber( + override val source: String, + override val column: Int, + val value: Int + ) : Token() { + override fun toReadableString(): String = value.toString() + } + + /** Corresponds to a decimal number (note that the decimal is required): ([0-9]*.[0-9]+) */ + data class DecimalNumber( + override val source: String, + override val column: Int, + val value: Double + ) : Token() { + override fun toReadableString(): String = value.toString() + } + + /** Corresponds to a variable identifier (single character): [a-z] */ + data class Identifier( + override val source: String, + override val column: Int, + val name: String + ) : Token() { + override fun toReadableString(): String = name + } + + /** Corresponds to an open parenthesis: ( */ + data class OpenParenthesis(override val source: String, override val column: Int) : Token() { + override fun toReadableString(): String = "(" + } + + /** Corresponds to a close parenthesis: ) */ + data class CloseParenthesis(override val source: String, override val column: Int) : Token() { + override fun toReadableString(): String = ")" + } + + /** Corresponds to an invalid token that was encountered. */ + data class InvalidToken( + override val source: String, + override val column: Int, + val token: String + ) : Token() { + override fun toReadableString(): String = "Invalid token: $token" + } + } + + companion object { + /** + * Returns an iterable that provides a lazy iterator over tokens that will only parse tokens as + * requested. + * + * Note that the returned iterable is thread-safe, but the iterator it provides is not. Callers + * should fully tokenize the stream by copying the iterable to list before performing + * multi-threaded operations on the results, or synchronize access to the iterator. + * + * Note also that tokenization is done agnostic of casing. + */ + fun tokenize(rawLiteral: String): Iterable { + return object : Iterable { + override fun iterator(): Iterator = + Tokenizer(rawLiteral.toLowerCase(Locale.getDefault())) + } + } + + /** + * Tokenizer for math expressions. Standard whitespace is ignored (including newlines). See + * subclasses of the Token class for valid tokens & their corresponding patterns. + * + * Note that this class is only safe to access on a single thread. + */ + private class Tokenizer(private val source: String) : Iterator { + private val buffer = source.toCharArray() + private var currentIndex = 0 + private var nextToken: Token? = null + + override fun hasNext(): Boolean = maybeParseNextToken() + + override fun next(): Token { + if (!hasNext()) { + throw IllegalStateException("Reach end-of-stream") + } + val token = checkNotNull(nextToken) { + "Encountered comodification in iterator: iterator modified during tokenization" + } + + // Reset the token so the next one can be parsed. + nextToken = null + + return token + } + + private fun maybeParseNextToken(): Boolean { + if (nextToken != null) { + // There's already a token parsed. + return true + } + + // Skip all whitespace before looking for new tokens. + skipWhitespace() + if (isAtEof()) { + // Reach the end of the stream. + return false + } + + // There's a token to parse. Parse it & continue. + nextToken = parseNextToken() + return true + } + + /** Returns whether the tokenizer has reached the end of the stream. */ + private fun isAtEof(): Boolean = isEofIndex(currentIndex) + + /** Skips whitespace. May be called if already at the end of the stream. */ + private fun skipWhitespace() { + advanceIndexTo(seekUntilCharacterNotFound(VALID_WHITESPACE)) + } + + /** Returns the next parsed token. Must not be called at the end of the stream. */ + private fun parseNextToken(): Token { + // Parse the next token in an order to avoid potential ambiguities (such as between whole & + // decimal numbers). + return parseOperator() + ?: parseDecimalNumber() + ?: parseWholeNumber() + ?: parseWholeNumber() + ?: parseIdentifier() + ?: parseParenthesis() + ?: parseInvalidToken() + } + + /** + * Returns the next operator token or null if the next token is not an operator. Must not be + * called at the end of the stream. + */ + private fun parseOperator(): Token? { + val parsedIndex = currentIndex + val potentialOperator = buffer[currentIndex] + if (potentialOperator !in VALID_OPERATORS) { + // The next character is not a recognized operator. + return null + } + + skipToken() + return Token.Operator(source, parsedIndex, potentialOperator) + } + + /** + * Returns the next decimal token or null if the next token is not a decimal number. Must not + * be called at the end of the stream. + */ + private fun parseDecimalNumber(): Token? { + val parsedIndex = currentIndex + val decimalIndex = seekUntilCharacterNotFound(VALID_DIGITS) + if (isEofIndex(decimalIndex) || buffer[decimalIndex] != '.') { + // There is nothing in the stream looking like: [0-9]*\\. + return null + } + + val numberEndIndex = seekUntilCharacterNotFound(VALID_DIGITS, startIndex = decimalIndex + 1) + if (numberEndIndex == decimalIndex + 1) { + // There are no digits following the period so this isn't a valid decimal. This may + // indicate an incorrectly formatted decimal, but the '.' will be picked up in a later + // token pass. + return null + } + + // Either the decimal is something.something, or just .something + val value = source.substring( + startIndex = currentIndex, + endIndex = numberEndIndex + ).toDouble() + advanceIndexTo(numberEndIndex) + return Token.DecimalNumber(source, parsedIndex, value) + } + + /** + * Returns the next whole number token or null if the next token is not a whole number. Must + * not be called at the end of the stream. + */ + private fun parseWholeNumber(): Token? { + val parsedIndex = currentIndex + val numberEndIndex = seekUntilCharacterNotFound(VALID_DIGITS) + if (currentIndex == numberEndIndex) { + // The next character is not a digit, so this can't be a whole number. + return null + } + + // Ensure the decimal is parsed in base 10 (in case it starts with 0--that shouldn't be + // interpreted as Octal). + val value = source.substring( + startIndex = currentIndex, + endIndex = numberEndIndex + ).toInt(radix = 10) + advanceIndexTo(numberEndIndex) + return Token.WholeNumber(source, parsedIndex, value) + } + + /** + * Returns the next identifier token or null if the next token is not an identifier. Must not + * be called at the end of the stream. + */ + private fun parseIdentifier(): Token? { + val parsedIndex = currentIndex + val potentialIdentifier = buffer[currentIndex] + if (potentialIdentifier !in VALID_IDENTIFIER_LETTERS) { + // The next character is not a recognized identifier letter. + return null + } + + skipToken() + return Token.Identifier(source, parsedIndex, potentialIdentifier.toString()) + } + + /** + * Returns the next parenthesis token (open or close) or null if the next token is not a + * parenthesis. Must not be called at the end of the stream. + */ + private fun parseParenthesis(): Token? { + val parsedIndex = currentIndex + return when (buffer[currentIndex]) { + '(' -> { + skipToken() + Token.OpenParenthesis(source, parsedIndex) + } + ')' -> { + skipToken() + Token.CloseParenthesis(source, parsedIndex) + } + else -> null + } + } + + /** + * Returns an invalid token for the next character in the stream. Must not be called at the + * end of the stream. + */ + private fun parseInvalidToken(): Token { + val parsedIndex = currentIndex + val errorCharacter = buffer[currentIndex] + // Skip the error token to try and recover to continue tokenizing. + skipToken() + return Token.InvalidToken(source, parsedIndex, errorCharacter.toString()) + } + + /** Returns whether the specified index as at the end of the stream. */ + private fun isEofIndex(index: Int): Boolean = index == buffer.size + + /** + * Advances the current index to the specified index (must be bigger than or equal to current + * index) and should not exceed the length of the stream. + */ + private fun advanceIndexTo(newIndex: Int) { + currentIndex = newIndex.coerceIn(currentIndex..buffer.size) + } + + /** Skips the next token in the stream. */ + private fun skipToken() = advanceIndexTo(currentIndex + 1) + + /** + * Returns the first index not matching the specified list, or the stream length if the rest + * of the stream matches. + */ + private fun seekUntilCharacterNotFound( + matchingChars: List, + startIndex: Int = currentIndex + ): Int { + var advanceIndex = startIndex + while (!isEofIndex(advanceIndex) && buffer[advanceIndex] in matchingChars) advanceIndex++ + return advanceIndex + } + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt new file mode 100644 index 00000000000..4240a6c5634 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -0,0 +1,66 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.Fraction +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.Real +import org.oppia.android.util.math.MathExpressionParser.Companion.ParseResult.Success +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class MathExpressionParserTest { +// @Test +// fun testParse_emptyString_returnsFailure() { +// } + + // test different parenthesis errors + // test unary operators + // test nested parenthesis + // test associativity (left & right) + // test order of operations + // test multiple variables + // test reals vs. whole numbers + // test nested operations for each operator + + // TODO: add support for implied multiplication (e.g. 'xy' should imply x*y). Ditto for coefficients. + + @Test + fun testParse_constantNumber_returnsExpressionWithFractionWholeNumber() { +// val result = MathExpressionParser.parseExpression("1") + //val result = MathExpressionParser.parseExpression("133 + 3.14 * x / (11 - 15) ^ 2 ^ 3") +// val result = MathExpressionParser.parseExpression("3 + 4 * 2 / (1 - 5) ^ 2 ^ 3") + // Test: 10/-1*-2 to verify unary precedence. + // Test unary at: beginning, after open paren, close paren (should be minus), and after operators + // Test multiple (2 & 3) unaries after each of the above + // Test invalid operator & operand cases, e.g. multiple operands or operators in wrong place + +// val result = MathExpressionParser.parseExpression("x^2*y^2 + 2") // works +// val result = MathExpressionParser.parseExpression("(x-1)^3") // works +// val result = MathExpressionParser.parseExpression("(x+1)/2") // fails +// val result = MathExpressionParser.parseExpression("x^2-3*x-10") // works +// val result = MathExpressionParser.parseExpression("x+2") // works +// val result = MathExpressionParser.parseExpression("4*(x+2)") // works + val result = MathExpressionParser.parseExpression("(x^2-3*x-10)*(x+2)") // works +// val result = MathExpressionParser.parseExpression("(x^2-3*x-10)/(x+2)") // fails + val polynomial = (result as Success).mathExpression.toPolynomial() + + println("@@@@@ Result: ${(result as Success).mathExpression}}") + println("@@@@@ Polynomial: $polynomial") + println("@@@@@ Polynomial str: ${polynomial?.toAnswerString()}") + + assertThat(result).isInstanceOf(Success::class.java) + val expression = (result as Success).mathExpression + assertThat(expression.expressionTypeCase).isEqualTo(MathExpression.ExpressionTypeCase.CONSTANT) + assertThat(expression.constant.realTypeCase).isEqualTo(Real.RealTypeCase.RATIONAL) + assertThat(expression.constant.rational).isEqualTo(createWholeNumberFraction(1)) + } + + private fun createWholeNumberFraction(value: Int): Fraction { + return Fraction.newBuilder().setWholeNumber(value).build() + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt new file mode 100644 index 00000000000..5c7cd303548 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt @@ -0,0 +1,286 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.util.math.MathTokenizer.Token.CloseParenthesis +import org.oppia.android.util.math.MathTokenizer.Token.DecimalNumber +import org.oppia.android.util.math.MathTokenizer.Token.Identifier +import org.oppia.android.util.math.MathTokenizer.Token.InvalidToken +import org.oppia.android.util.math.MathTokenizer.Token.OpenParenthesis +import org.oppia.android.util.math.MathTokenizer.Token.Operator +import org.oppia.android.util.math.MathTokenizer.Token.WholeNumber +import org.robolectric.annotation.LooperMode + +/** Tests for [MathTokenizer]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class MathTokenizerTest { + @Test + fun testTokenize_emptyString_producesNoTokens() { + val tokens = MathTokenizer.tokenize("").toList() + + assertThat(tokens).isEmpty() + } + + @Test + fun testTokenize_wholeNumber_oneDigit_producesWholeNumberToken() { + val tokens = MathTokenizer.tokenize("1").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(WholeNumber::class.java) + assertThat((tokens.first() as WholeNumber).value).isEqualTo(1) + } + + @Test + fun testTokenize_wholeNumber_multipleDigits_producesWholeNumberToken() { + val tokens = MathTokenizer.tokenize("913").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(WholeNumber::class.java) + assertThat((tokens.first() as WholeNumber).value).isEqualTo(913) + } + + @Test + fun testTokenize_wholeNumber_zeroLeadingNumber_producesCorrectBase10WholeNumberToken() { + val tokens = MathTokenizer.tokenize("0913").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(WholeNumber::class.java) + assertThat((tokens.first() as WholeNumber).value).isEqualTo(913) + } + + @Test + fun testTokenize_decimalNumber_decimalLessThanOne_noZero_producesCorrectDecimalNumberToken() { + val tokens = MathTokenizer.tokenize(".14").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(DecimalNumber::class.java) + assertThat((tokens.first() as DecimalNumber).value).isWithin(1e-3).of(0.14) + } + + @Test + fun testTokenize_decimalNumber_decimalLessThanOne_withZero_producesCorrectDecimalNumberToken() { + val tokens = MathTokenizer.tokenize("0.14").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(DecimalNumber::class.java) + assertThat((tokens.first() as DecimalNumber).value).isWithin(1e-3).of(0.14) + } + + @Test + fun testTokenize_decimalNumber_decimalGreaterThanOne_producesCorrectDecimalNumberToken() { + val tokens = MathTokenizer.tokenize("3.14").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(DecimalNumber::class.java) + assertThat((tokens.first() as DecimalNumber).value).isWithin(1e-3).of(3.14) + } + + @Test + fun testTokenize_decimalNumber_decimalPointOnly_producesInvalidToken() { + val tokens = MathTokenizer.tokenize(".").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(InvalidToken::class.java) + assertThat((tokens.first() as InvalidToken).token).isEqualTo(".") + } + + @Test + fun testTokenize_openParenthesis_producesOpenParenthesisToken() { + val tokens = MathTokenizer.tokenize("(").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(OpenParenthesis::class.java) + } + + @Test + fun testTokenize_closeParenthesis_producesCloseParenthesisToken() { + val tokens = MathTokenizer.tokenize(")").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(CloseParenthesis::class.java) + } + + @Test + fun testTokenize_plusSign_producesOperatorToken() { + val tokens = MathTokenizer.tokenize("+").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(Operator::class.java) + assertThat((tokens.first() as Operator).operator).isEqualTo('+') + } + + @Test + fun testTokenize_minusSign_producesOperatorToken() { + val tokens = MathTokenizer.tokenize("-").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(Operator::class.java) + assertThat((tokens.first() as Operator).operator).isEqualTo('-') + } + + @Test + fun testTokenize_asterisk_producesOperatorToken() { + val tokens = MathTokenizer.tokenize("*").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(Operator::class.java) + assertThat((tokens.first() as Operator).operator).isEqualTo('*') + } + + @Test + fun testTokenize_forwardSlash_producesOperatorToken() { + val tokens = MathTokenizer.tokenize("/").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(Operator::class.java) + assertThat((tokens.first() as Operator).operator).isEqualTo('/') + } + + @Test + fun testTokenize_caret_producesOperatorToken() { + val tokens = MathTokenizer.tokenize("^").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(Operator::class.java) + assertThat((tokens.first() as Operator).operator).isEqualTo('^') + } + + @Test + fun testTokenize_exclamation_producesInvalidToken() { + val tokens = MathTokenizer.tokenize("!").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(InvalidToken::class.java) + assertThat((tokens.first() as InvalidToken).token).isEqualTo("!") + } + + @Test + fun testTokenize_identifier_producesIdentifierToken() { + val tokens = MathTokenizer.tokenize("x").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(Identifier::class.java) + assertThat((tokens.first() as Identifier).name).isEqualTo("x") + } + + @Test + fun testTokenize_multipleIdentifiers_withoutSpaces_producesIdentifierTokensForEachInOrder() { + val tokens = MathTokenizer.tokenize("xyz").toList() + + assertThat(tokens).hasSize(3) + assertThat(tokens[0]).isInstanceOf(Identifier::class.java) + assertThat(tokens[1]).isInstanceOf(Identifier::class.java) + assertThat(tokens[2]).isInstanceOf(Identifier::class.java) + assertThat((tokens[0] as Identifier).name).isEqualTo("x") + assertThat((tokens[1] as Identifier).name).isEqualTo("y") + assertThat((tokens[2] as Identifier).name).isEqualTo("z") + } + + @Test + fun testTokenize_multipleIdentifiers_withSpaces_producesIdentifierTokensForEachInOrder() { + val tokens = MathTokenizer.tokenize("x y z").toList() + + assertThat(tokens).hasSize(3) + assertThat(tokens[0]).isInstanceOf(Identifier::class.java) + assertThat(tokens[1]).isInstanceOf(Identifier::class.java) + assertThat(tokens[2]).isInstanceOf(Identifier::class.java) + assertThat((tokens[0] as Identifier).name).isEqualTo("x") + assertThat((tokens[1] as Identifier).name).isEqualTo("y") + assertThat((tokens[2] as Identifier).name).isEqualTo("z") + } + + @Test + fun testTokenize_identifier_whitespaceBefore_isIgnored() { + val tokens = MathTokenizer.tokenize(" \r\t\n x").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(Identifier::class.java) + assertThat((tokens.first() as Identifier).name).isEqualTo("x") + } + + @Test + fun testTokenize_identifier_whitespaceAfter_isIgnored() { + val tokens = MathTokenizer.tokenize("x \r\t\n ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(Identifier::class.java) + assertThat((tokens.first() as Identifier).name).isEqualTo("x") + } + + @Test + fun testTokenize_identifierAndOperator_whitespaceBetween_isIgnored() { + val tokens = MathTokenizer.tokenize("- \r\t\n x").toList() + + assertThat(tokens).hasSize(2) + assertThat(tokens[0]).isInstanceOf(Operator::class.java) + assertThat(tokens[1]).isInstanceOf(Identifier::class.java) + assertThat((tokens[0] as Operator).operator).isEqualTo('-') + assertThat((tokens[1] as Identifier).name).isEqualTo("x") + } + + @Test + fun testTokenize_digits_withSpaces_producesMultipleWholeNumberTokens() { + val tokens = MathTokenizer.tokenize("1 23 4").toList() + + assertThat(tokens).hasSize(3) + assertThat(tokens[0]).isInstanceOf(WholeNumber::class.java) + assertThat(tokens[1]).isInstanceOf(WholeNumber::class.java) + assertThat(tokens[2]).isInstanceOf(WholeNumber::class.java) + assertThat((tokens[0] as WholeNumber).value).isEqualTo(1) + assertThat((tokens[1] as WholeNumber).value).isEqualTo(23) + assertThat((tokens[2] as WholeNumber).value).isEqualTo(4) + } + + @Test + fun testTokenize_complexExpressionWithAllTokenTypes_tokenizesEverythingInOrder() { + val tokens = MathTokenizer.tokenize("133 + 3.14 * x / (11 - 15) ^ 2 ^ 3").toList() + + assertThat(tokens).hasSize(15) + + assertThat(tokens[0]).isInstanceOf(WholeNumber::class.java) + assertThat((tokens[0] as WholeNumber).value).isEqualTo(133) + + assertThat(tokens[1]).isInstanceOf(Operator::class.java) + assertThat((tokens[1] as Operator).operator).isEqualTo('+') + + assertThat(tokens[2]).isInstanceOf(DecimalNumber::class.java) + assertThat((tokens[2] as DecimalNumber).value).isWithin(1e-3).of(3.14) + + assertThat(tokens[3]).isInstanceOf(Operator::class.java) + assertThat((tokens[3] as Operator).operator).isEqualTo('*') + + assertThat(tokens[4]).isInstanceOf(Identifier::class.java) + assertThat((tokens[4] as Identifier).name).isEqualTo("x") + + assertThat(tokens[5]).isInstanceOf(Operator::class.java) + assertThat((tokens[5] as Operator).operator).isEqualTo('/') + + assertThat(tokens[6]).isInstanceOf(OpenParenthesis::class.java) + + assertThat(tokens[7]).isInstanceOf(WholeNumber::class.java) + assertThat((tokens[7] as WholeNumber).value).isEqualTo(11) + + assertThat(tokens[8]).isInstanceOf(Operator::class.java) + assertThat((tokens[8] as Operator).operator).isEqualTo('-') + + assertThat(tokens[9]).isInstanceOf(WholeNumber::class.java) + assertThat((tokens[9] as WholeNumber).value).isEqualTo(15) + + assertThat(tokens[10]).isInstanceOf(CloseParenthesis::class.java) + + assertThat(tokens[11]).isInstanceOf(Operator::class.java) + assertThat((tokens[11] as Operator).operator).isEqualTo('^') + + assertThat(tokens[12]).isInstanceOf(WholeNumber::class.java) + assertThat((tokens[12] as WholeNumber).value).isEqualTo(2) + + assertThat(tokens[13]).isInstanceOf(Operator::class.java) + assertThat((tokens[13] as Operator).operator).isEqualTo('^') + + assertThat(tokens[14]).isInstanceOf(WholeNumber::class.java) + assertThat((tokens[14] as WholeNumber).value).isEqualTo(3) + } +} From 062b34dc37ae9137bf386a372e8b5e924ffbcac5 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 9 Dec 2020 12:19:21 -0800 Subject: [PATCH 051/289] Address reviewer comments. --- .github/CODEOWNERS | 1 + .github/workflows/workflow_canceller.yml | 9 +++++++++ scripts/compute_affected_tests.sh | 8 ++++---- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 75bc528e7cd..c73cd3405c7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,6 +13,7 @@ # GitHub Actions & CI workflows. .github/*.yaml @BenHenning +.github/*.yml @BenHenning # Blanket codeowners # This is for the case when new files are created in any directories that aren't diff --git a/.github/workflows/workflow_canceller.yml b/.github/workflows/workflow_canceller.yml index b3402485e7a..fd6ac56313a 100644 --- a/.github/workflows/workflow_canceller.yml +++ b/.github/workflows/workflow_canceller.yml @@ -1,5 +1,13 @@ name: Automatic Workflow Canceller +# This workflow should be triggered in one of three situations: +# 1. Manual workflow dispatch via https://github.com/oppia/oppia-android/actions. +# 2. Upon creation of a PR & updates to that PR. +# 3. Whenever the develop branch is changed (e.g. after a PR is merged). +# +# Note that the action being used here automatically accounts for the current branch & the commit +# hash of the tip of the branch to ensure it doesn't cancel previous workflows that aren't related +# to the branch being evaluated. on: workflow_dispatch: pull_request: @@ -13,6 +21,7 @@ jobs: name: Cancel Previous Runs runs-on: ubuntu-18.04 steps: + # See https://github.com/styfle/cancel-workflow-action for details on this workflow. - uses: styfle/cancel-workflow-action@0.6.0 with: workflow_id: main.yml diff --git a/scripts/compute_affected_tests.sh b/scripts/compute_affected_tests.sh index 0b4990a47e0..e264b37d824 100755 --- a/scripts/compute_affected_tests.sh +++ b/scripts/compute_affected_tests.sh @@ -39,21 +39,21 @@ if [[ "$current_branch" != "develop" ]]; then # Filter all of the source files among those that are actually included in Bazel builds. changed_bazel_files=() - for changed_file in ${changed_files[@]}; do + for changed_file in "${changed_files[@]}"; do changed_bazel_files+=($($BAZEL_BINARY query --noshow_progress $changed_file 2> /dev/null)) done # Compute the list of affected tests based on source files. - source_affected_targets=$($BAZEL_BINARY query --noshow_progress --universe_scope=//... --order_output=no "kind(test, allrdeps(set(${changed_bazel_files[@]})))" 2>/dev/null) + source_affected_targets="$($BAZEL_BINARY query --noshow_progress --universe_scope=//... --order_output=no "kind(test, allrdeps(set(${changed_bazel_files[@]})))" 2>/dev/null)" # Compute the list of files to consider for BUILD-level changes (this uses the base file list as a # reference since Bazel's query won't find matching targets for utility bzl files that can still # affect the build). https://stackoverflow.com/a/44107086 for reference on changing case matching. shopt -s nocasematch changed_bazel_support_files=() - for changed_file in ${changed_files[@]}; do + for changed_file in "${changed_files[@]}"; do if [[ "$changed_file" =~ ^.+?\.bazel$ ]] || [[ "$changed_file" =~ ^.+?\.bzl$ ]] || [[ "$changed_file" == "WORKSPACE" ]]; then - changed_bazel_support_files+=($changed_file) + changed_bazel_support_files+=("$changed_file") fi done shopt -u nocasematch From 6c8958a9d84b118958d2c05e5c7d6ee495839e46 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 9 Dec 2020 18:12:07 -0800 Subject: [PATCH 052/289] Improve identifier & operator parsing support. For identifiers: added support for explicitly specifying allowed identifiers, including multi-letter identifiers. Any letters are also now allowed for IDs (including greek letters). For operators: added support for the formal multiplication & division symbols, but the implementation translates them to * and /, respectively, to simplifying upstream parsing. --- .../oppia/android/util/math/MathTokenizer.kt | 271 ++++++++++++++--- .../android/util/math/MathTokenizerTest.kt | 276 +++++++++++++++++- 2 files changed, 503 insertions(+), 44 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt index cce210cfd5c..339a5153e2d 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt @@ -2,12 +2,23 @@ package org.oppia.android.util.math import java.lang.IllegalStateException import java.util.Locale +import java.util.ArrayDeque + +private const val DECIMAL_POINT = '.' +private const val LEFT_PARENTHESIS = '(' +private const val RIGHT_PARENTHESIS = ')' +private const val CONVENTIONAL_MULTIPLICATION_SIGN = '*' +private const val CONVENTIONAL_DIVISION_SIGN = '/' +private const val FORMAL_MULTIPLICATION_SIGN = '×' +private const val FORMAL_DIVISION_SIGN = '÷' // Only consider standard horizontal/vertical whitespace. private val VALID_WHITESPACE = listOf(' ', '\t', '\n', '\r') -private val VALID_OPERATORS = listOf('*', '-', '+', '/', '^') +private val VALID_OPERATORS = listOf( + CONVENTIONAL_MULTIPLICATION_SIGN, '-', '+', CONVENTIONAL_DIVISION_SIGN, '^', + FORMAL_MULTIPLICATION_SIGN, FORMAL_DIVISION_SIGN +) private val VALID_DIGITS = listOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9') -private val VALID_IDENTIFIER_LETTERS = "abcdefghijklmnopqrstuvwxyz".toCharArray().toList() /** * Container class for functionality corresponding to tokenization of mathematics expressions. @@ -50,7 +61,7 @@ class MathTokenizer { override fun toReadableString(): String = value.toString() } - /** Corresponds to a variable identifier (single character): [a-z] */ + /** Corresponds to an identifier, single or multi-letter (typically for variables). */ data class Identifier( override val source: String, override val column: Int, @@ -61,19 +72,28 @@ class MathTokenizer { /** Corresponds to an open parenthesis: ( */ data class OpenParenthesis(override val source: String, override val column: Int) : Token() { - override fun toReadableString(): String = "(" + override fun toReadableString(): String = LEFT_PARENTHESIS.toString() } /** Corresponds to a close parenthesis: ) */ data class CloseParenthesis(override val source: String, override val column: Int) : Token() { - override fun toReadableString(): String = ")" + override fun toReadableString(): String = RIGHT_PARENTHESIS.toString() + } + + /** Corresponds to an invalid identifier that was encountered. */ + data class InvalidIdentifier( + override val source: String, + override val column: Int, + val name: String + ) : Token() { + override fun toReadableString(): String = "Invalid identifier: $name" } /** Corresponds to an invalid token that was encountered. */ data class InvalidToken( - override val source: String, - override val column: Int, - val token: String + override val source: String, + override val column: Int, + val token: String ) : Token() { override fun toReadableString(): String = "Invalid token: $token" } @@ -88,12 +108,37 @@ class MathTokenizer { * should fully tokenize the stream by copying the iterable to list before performing * multi-threaded operations on the results, or synchronize access to the iterator. * - * Note also that tokenization is done agnostic of casing. + * Note also that tokenization is done in a case-insensitive manner. + * + * Note also that both single-letter and multi-letter identifiers are supported, but their + * behaviors differ. Single-letter identifiers support implicit multiplication (e.g. 'xy' is + * equivalent to 'x*y') but multi-letter identifiers do not (e.g. pilambda) since in the latter + * case 'pilambda' is treated as a single, unknown identifier. Note that in cases of ambiguity, + * multi-letter identifiers take precedence. For example, if the provided identifiers are 'p', + * 'i', and 'pi', then encounters of 'pi' will use the multi-letter identifier rather than p*i. + * + * @param allowedIdentifiers a list of acceptable identifiers that can be parsed (these may be + * more than one letter long). By default, the identifiers 'x' and 'y' are used. Any + * identifiers encountered that aren't part of this list will result in an invalid + * identifier token being returned. Note that identifiers must only contain strings with + * letters (per the definition of Character.isLetter()). This list can be empty (in which + * case all encountered identifiers will be presumed invalid). */ - fun tokenize(rawLiteral: String): Iterable { + fun tokenize( + rawLiteral: String, + allowedIdentifiers: List = listOf("x", "y") + ): Iterable { + // Verify that the provided identifiers are all valid. + for (identifier in allowedIdentifiers) { + if (identifier.any(Char::isNotLetter)) { + throw IllegalArgumentException("Identifier contains non-letters: $identifier") + } + } + + val lowercaseLiteral = rawLiteral.toLowerCase(Locale.getDefault()) + val lowercaseIdentifiers = allowedIdentifiers.map { it.toLowerCase(Locale.getDefault()) } return object : Iterable { - override fun iterator(): Iterator = - Tokenizer(rawLiteral.toLowerCase(Locale.getDefault())) + override fun iterator(): Iterator = Tokenizer(lowercaseLiteral, lowercaseIdentifiers) } } @@ -103,7 +148,17 @@ class MathTokenizer { * * Note that this class is only safe to access on a single thread. */ - private class Tokenizer(private val source: String) : Iterator { + private class Tokenizer( + private val source: String, + private val allowedIdentifiers: List + ) : Iterator { + private val singleLetterIdentifiers: List by lazy { + allowedIdentifiers.filter { it.length == 1 }.map(String::first) + } + private val multiLetterIdentifiers: List by lazy { + allowedIdentifiers.filter { it.length > 1 } + } + private val parsedIdentifierCache = ArrayDeque() private val buffer = source.toCharArray() private var currentIndex = 0 private var nextToken: Token? = null @@ -130,14 +185,20 @@ class MathTokenizer { return true } - // Skip all whitespace before looking for new tokens. - skipWhitespace() - if (isAtEof()) { - // Reach the end of the stream. - return false + // Note that there's hidden caching built in for identifiers: identifier parsing can yield + // multiple identifiers and only one token is returned at a time, any previously parsed + // identifiers take top precedent. + if (parsedIdentifierCache.isEmpty()) { + // Skip all whitespace before looking for new tokens. + skipWhitespace() + if (isAtEof()) { + // Reach the end of the stream. + return false + } } - // There's a token to parse. Parse it & continue. + // Otherwise, there's a token to parse (either there's a pending variable or the + // end-of-stream has not yet been reached). Parse it & continue. nextToken = parseNextToken() return true } @@ -154,29 +215,44 @@ class MathTokenizer { private fun parseNextToken(): Token { // Parse the next token in an order to avoid potential ambiguities (such as between whole & // decimal numbers). - return parseOperator() + return retrieveNextParsedIdentifier() + ?: parseOperator() ?: parseDecimalNumber() ?: parseWholeNumber() ?: parseWholeNumber() - ?: parseIdentifier() + ?: parseIdentifiersAndReturnFirst() ?: parseParenthesis() ?: parseInvalidToken() } + /** + * Returns the next identifier in the local cache of parsed identifiers, or null if there are + * none left/available. + */ + private fun retrieveNextParsedIdentifier(): Token? = parsedIdentifierCache.poll() + /** * Returns the next operator token or null if the next token is not an operator. Must not be * called at the end of the stream. */ private fun parseOperator(): Token? { val parsedIndex = currentIndex - val potentialOperator = buffer[currentIndex] + val potentialOperator = peekCharacter() if (potentialOperator !in VALID_OPERATORS) { // The next character is not a recognized operator. return null } + // When interpreting the operator, translate the unicode symbols to conventional symbols to + // simplify upstream parsing. + val parsedOperator = when (potentialOperator) { + FORMAL_MULTIPLICATION_SIGN -> CONVENTIONAL_MULTIPLICATION_SIGN + FORMAL_DIVISION_SIGN -> CONVENTIONAL_DIVISION_SIGN + else -> potentialOperator + } + skipToken() - return Token.Operator(source, parsedIndex, potentialOperator) + return Token.Operator(source, parsedIndex, parsedOperator) } /** @@ -186,7 +262,7 @@ class MathTokenizer { private fun parseDecimalNumber(): Token? { val parsedIndex = currentIndex val decimalIndex = seekUntilCharacterNotFound(VALID_DIGITS) - if (isEofIndex(decimalIndex) || buffer[decimalIndex] != '.') { + if (isEofIndex(decimalIndex) || buffer[decimalIndex] != DECIMAL_POINT) { // There is nothing in the stream looking like: [0-9]*\\. return null } @@ -231,19 +307,107 @@ class MathTokenizer { } /** - * Returns the next identifier token or null if the next token is not an identifier. Must not - * be called at the end of the stream. + * Parses the next one or more identifiers and returns the first one, caching the others, or + * returns null if the immediate next token is not an identifier. + */ + private fun parseIdentifiersAndReturnFirst(): Token? { + parsedIdentifierCache += parseIdentifiers() ?: return null + return retrieveNextParsedIdentifier() + } + + /** + * Returns the next identifier tokens or null if the next character itself is not a token, or + * does not indicate one or more following identifier tokens. Must not be called at the end of + * the stream. */ - private fun parseIdentifier(): Token? { + private fun parseIdentifiers(): List? { val parsedIndex = currentIndex - val potentialIdentifier = buffer[currentIndex] - if (potentialIdentifier !in VALID_IDENTIFIER_LETTERS) { - // The next character is not a recognized identifier letter. - return null + val nextNonIdentifierIndex = seekUntil { it.isNotLetter() } + return when (nextNonIdentifierIndex - parsedIndex) { + // The next character is something other than a potential identifier. + 0 -> null + // Trivial case: there's a single letter identifier. + 1 -> listOf(parseSingleLetterIdentifier()) + // Complex case: either this is one multi-letter identifier, multiple single-letter + // identifiers with implied multiplication, or an invalid multi-letter identifier. + else -> parseValidMultiLetterIdentifier(nextNonIdentifierIndex) + ?: parseMultipleSingleLetterIdentifiers(nextNonIdentifierIndex) + ?: listOf(parseInvalidMultiLetterIdentifier(nextNonIdentifierIndex)) } + } + /** + * Returns the next token of the buffer as a single-letter identifier, or an invalid + * identifier if the character does not correspond to an allowed single-letter identifier. + */ + private fun parseSingleLetterIdentifier(): Token { + val parsedIndex = currentIndex + val potentialIdentifier = peekCharacter() skipToken() - return Token.Identifier(source, parsedIndex, potentialIdentifier.toString()) + return maybeParseSingleLetterIdentifier(parsedIndex) + ?: Token.InvalidIdentifier(source, parsedIndex, potentialIdentifier.toString()) + } + + /** + * Returns the next token of the buffer as a single-letter identifier, null if the character + * is not a valid single-letter identifier. Note that this does not change the underlying + * stream state (i.e. it does not skip the parsed token)--the caller is expected to do that. + * The caller is also expected to guarantee that the provided parsedIndex is within the + * buffer. + */ + private fun maybeParseSingleLetterIdentifier(parsedIndex: Int): Token? { + val potentialIdentifier = buffer[parsedIndex] + return if (potentialIdentifier in singleLetterIdentifiers) { + Token.Identifier(source, parsedIndex, potentialIdentifier.toString()) + } else null + } + + /** + * Returns the next set of characters up to nextNonIdentifierIndex as a multi-letter + * identifier, or null if those characters do not correspond to a valid multi-letter + * identifier. Note that the returned list will always contain a single token corresponding to + * the multi-letter identifier, or the whole list will be null if parsing failed. + */ + private fun parseValidMultiLetterIdentifier(nextNonIdentifierIndex: Int): List? { + val parsedIndex = currentIndex + val potentialIdentifier = extractSubBufferString( + startIndex = currentIndex, + endIndex = nextNonIdentifierIndex + ) + return if (potentialIdentifier in multiLetterIdentifiers) { + advanceIndexTo(nextNonIdentifierIndex) + listOf(Token.Identifier(source, parsedIndex, potentialIdentifier)) + } else null + } + + /** + * Returns a list of single-letter identifiers for all characters up to the specified index, + * or null if any characters encountered are not valid single-letter identifiers. + */ + private fun parseMultipleSingleLetterIdentifiers(nextNonIdentifierIndex: Int): List? { + val singleLetterIdentifiers = mutableListOf() + for (parsedIndex in currentIndex until nextNonIdentifierIndex) { + singleLetterIdentifiers += maybeParseSingleLetterIdentifier(parsedIndex) ?: return null + } + // Skip all of the characters encountered if each one corresponds to a valid identifier. + advanceIndexTo(nextNonIdentifierIndex) + return singleLetterIdentifiers + } + + /** + * Returns a token indicating that all characters from the current index to the specified + * index (but not including that index) correspond to an invalid multi-letter identifier. + */ + private fun parseInvalidMultiLetterIdentifier(nextNonIdentifierIndex: Int): Token { + // Assume all characters between currentIndex and nextNonIdentifierIndex (exclusive) + // comprise a single, unknown multi-letter identifier. + val parsedIndex = currentIndex + advanceIndexTo(nextNonIdentifierIndex) + return Token.InvalidIdentifier( + source, + parsedIndex, + extractSubBufferString(startIndex = parsedIndex, endIndex = nextNonIdentifierIndex) + ) } /** @@ -252,12 +416,12 @@ class MathTokenizer { */ private fun parseParenthesis(): Token? { val parsedIndex = currentIndex - return when (buffer[currentIndex]) { - '(' -> { + return when (peekCharacter()) { + LEFT_PARENTHESIS -> { skipToken() Token.OpenParenthesis(source, parsedIndex) } - ')' -> { + RIGHT_PARENTHESIS -> { skipToken() Token.CloseParenthesis(source, parsedIndex) } @@ -271,12 +435,27 @@ class MathTokenizer { */ private fun parseInvalidToken(): Token { val parsedIndex = currentIndex - val errorCharacter = buffer[currentIndex] + val errorCharacter = peekCharacter() // Skip the error token to try and recover to continue tokenizing. skipToken() return Token.InvalidToken(source, parsedIndex, errorCharacter.toString()) } + /** + * Returns the next character in the buffer. Should only be called when not at the end of the + * stream. + */ + private fun peekCharacter(): Char = buffer[currentIndex] + + /** + * Returns a string representation of a cut of the stream buffer starting at the specified + * index and up to, but not including, the specified end index. It's assumed the caller + * ensures that 0 <= startIndex <= endIndex < buffer.size. + */ + private fun extractSubBufferString(startIndex: Int, endIndex: Int): String { + return String(chars = buffer, offset = startIndex, length = endIndex - startIndex) + } + /** Returns whether the specified index as at the end of the stream. */ private fun isEofIndex(index: Int): Boolean = index == buffer.size @@ -291,6 +470,16 @@ class MathTokenizer { /** Skips the next token in the stream. */ private fun skipToken() = advanceIndexTo(currentIndex + 1) + /** + * Returns the index of the first character not matching the specified predicate, or the + * stream length if the rest of the stream matches. + */ + private fun seekUntil(startIndex: Int = currentIndex, predicate: (Char) -> Boolean): Int { + var advanceIndex = startIndex + while (!isEofIndex(advanceIndex) && !predicate(buffer[advanceIndex])) advanceIndex++ + return advanceIndex + } + /** * Returns the first index not matching the specified list, or the stream length if the rest * of the stream matches. @@ -298,11 +487,11 @@ class MathTokenizer { private fun seekUntilCharacterNotFound( matchingChars: List, startIndex: Int = currentIndex - ): Int { - var advanceIndex = startIndex - while (!isEofIndex(advanceIndex) && buffer[advanceIndex] in matchingChars) advanceIndex++ - return advanceIndex - } + ): Int = seekUntil(startIndex) { it !in matchingChars } } } } + +private fun Char.isNotLetter(): Boolean { + return !isLetter() +} diff --git a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt index 5c7cd303548..3320bd88250 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt @@ -7,16 +7,24 @@ import org.junit.runner.RunWith import org.oppia.android.util.math.MathTokenizer.Token.CloseParenthesis import org.oppia.android.util.math.MathTokenizer.Token.DecimalNumber import org.oppia.android.util.math.MathTokenizer.Token.Identifier +import org.oppia.android.util.math.MathTokenizer.Token.InvalidIdentifier import org.oppia.android.util.math.MathTokenizer.Token.InvalidToken import org.oppia.android.util.math.MathTokenizer.Token.OpenParenthesis import org.oppia.android.util.math.MathTokenizer.Token.Operator import org.oppia.android.util.math.MathTokenizer.Token.WholeNumber import org.robolectric.annotation.LooperMode +import kotlin.reflect.KClass +import kotlin.reflect.full.cast +import kotlin.test.fail /** Tests for [MathTokenizer]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class MathTokenizerTest { + private val ALLOWED_XYZ_VARIABLES = listOf("x", "y", "z") + private val ALLOWED_XYZ_WITH_LAMBDA_VARIABLES = ALLOWED_XYZ_VARIABLES + listOf("lambda") + private val ALLOWED_XYZ_WITH_COMBINED_XYZ_VARIABLES = ALLOWED_XYZ_VARIABLES + listOf("xyz") + @Test fun testTokenize_emptyString_producesNoTokens() { val tokens = MathTokenizer.tokenize("").toList() @@ -130,6 +138,16 @@ class MathTokenizerTest { assertThat((tokens.first() as Operator).operator).isEqualTo('*') } + @Test + fun testTokenize_formalMultiplicationSign_producesAsteriskOperatorToken() { + val tokens = MathTokenizer.tokenize("×").toList() + + // The formal math multiplication symbol is translated to the conventional one for simplicity. + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(Operator::class.java) + assertThat((tokens.first() as Operator).operator).isEqualTo('*') + } + @Test fun testTokenize_forwardSlash_producesOperatorToken() { val tokens = MathTokenizer.tokenize("/").toList() @@ -139,6 +157,16 @@ class MathTokenizerTest { assertThat((tokens.first() as Operator).operator).isEqualTo('/') } + @Test + fun testTokenize_formalDivisionSign_producesForwardSlashOperatorToken() { + val tokens = MathTokenizer.tokenize("÷").toList() + + // The formal math division symbol is translated to the conventional one for simplicity. + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(Operator::class.java) + assertThat((tokens.first() as Operator).operator).isEqualTo('/') + } + @Test fun testTokenize_caret_producesOperatorToken() { val tokens = MathTokenizer.tokenize("^").toList() @@ -158,7 +186,7 @@ class MathTokenizerTest { } @Test - fun testTokenize_identifier_producesIdentifierToken() { + fun testTokenize_defaultIdentifier_producesIdentifierToken() { val tokens = MathTokenizer.tokenize("x").toList() assertThat(tokens).hasSize(1) @@ -166,10 +194,124 @@ class MathTokenizerTest { assertThat((tokens.first() as Identifier).name).isEqualTo("x") } + @Test + fun testTokenize_defaultIdentifier_withNoIdentifiersProvided_producesInvalidIdentifierToken() { + val tokens = MathTokenizer.tokenize("x", allowedIdentifiers = listOf()).toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(InvalidIdentifier::class.java) + assertThat((tokens.first() as InvalidIdentifier).name).isEqualTo("x") + } + + @Test + fun testTokenize_defaultIdentifier_withInvalidAllowedIdentifiers_throwsException() { + val exception = assertThrows(IllegalArgumentException::class) { + MathTokenizer.tokenize("x", allowedIdentifiers = listOf("valid", "invalid!")).toList() + } + + assertThat(exception).hasMessageThat().contains("contains non-letters: invalid!") + } + + @Test + fun testTokenize_nonDefaultIdentifier_withDefaultIdentifiers_producesInvalidIdentifierToken() { + val tokens = MathTokenizer.tokenize("z").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(InvalidIdentifier::class.java) + assertThat((tokens.first() as InvalidIdentifier).name).isEqualTo("z") + } + + @Test + fun testTokenize_nonDefaultIdentifier_withAllowedIdentifiers_producesIdentifierToken() { + val tokens = MathTokenizer.tokenize("z", allowedIdentifiers = listOf("z")).toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(Identifier::class.java) + assertThat((tokens.first() as Identifier).name).isEqualTo("z") + } + + @Test + fun testTokenize_nonDefaultIdentifierLowercase_withAllowedIdentifiersUpper_producesIdToken() { + val tokens = MathTokenizer.tokenize("z", allowedIdentifiers = listOf("Z")).toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(Identifier::class.java) + assertThat((tokens.first() as Identifier).name).isEqualTo("z") + } + + @Test + fun testTokenize_nonDefaultIdentifierUppercase_withAllowedIdentifiersLower_producesIdToken() { + val tokens = MathTokenizer.tokenize("Z", allowedIdentifiers = listOf("z")).toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(Identifier::class.java) + assertThat((tokens.first() as Identifier).name).isEqualTo("z") + } + + @Test + fun testTokenize_nonDefaultIdentifierUppercase_withAllowedIdentifiersUpper_producesIdToken() { + val tokens = MathTokenizer.tokenize("Z", allowedIdentifiers = listOf("Z")).toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(Identifier::class.java) + assertThat((tokens.first() as Identifier).name).isEqualTo("z") + } + + @Test + fun testTokenize_greekLetterIdentifier_withAllowedIdentifiers_producesIdentifierToken() { + val tokens = MathTokenizer.tokenize("π", allowedIdentifiers = listOf("π")).toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(Identifier::class.java) + assertThat((tokens.first() as Identifier).name).isEqualTo("π") + } + + @Test + fun testTokenize_greekLetterIdentifier_withAllowedIdentifiersUppercase_producesIdentifierToken() { + val tokens = MathTokenizer.tokenize("π", allowedIdentifiers = listOf("Π")).toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(Identifier::class.java) + assertThat((tokens.first() as Identifier).name).isEqualTo("π") + } + @Test fun testTokenize_multipleIdentifiers_withoutSpaces_producesIdentifierTokensForEachInOrder() { + val tokens = MathTokenizer.tokenize("xyz", ALLOWED_XYZ_VARIABLES).toList() + + assertThat(tokens).hasSize(3) + assertThat(tokens[0]).isInstanceOf(Identifier::class.java) + assertThat(tokens[1]).isInstanceOf(Identifier::class.java) + assertThat(tokens[2]).isInstanceOf(Identifier::class.java) + assertThat((tokens[0] as Identifier).name).isEqualTo("x") + assertThat((tokens[1] as Identifier).name).isEqualTo("y") + assertThat((tokens[2] as Identifier).name).isEqualTo("z") + } + + @Test + fun testTokenize_validMultiWordIdentifier_producesSingleIdentifierToken() { + val tokens = MathTokenizer.tokenize("lambda", allowedIdentifiers = listOf("lambda")).toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(Identifier::class.java) + assertThat((tokens.first() as Identifier).name).isEqualTo("lambda") + } + + @Test + fun testTokenize_invalidMultiWordIdentifier_missingFromAllowedList_producesInvalidIdToken() { val tokens = MathTokenizer.tokenize("xyz").toList() + // Note that even though 'x' and 'y' are valid single-letter variables, because 'z' is + // encountered the whole set of letters is considered a single invalid variable. + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(InvalidIdentifier::class.java) + assertThat((tokens.first() as InvalidIdentifier).name).isEqualTo("xyz") + } + + @Test + fun testTokenize_multipleIdentifiers_singleLetter_withSpaces_producesIdTokensForEachInOrder() { + val tokens = MathTokenizer.tokenize("x y z", ALLOWED_XYZ_VARIABLES).toList() + assertThat(tokens).hasSize(3) assertThat(tokens[0]).isInstanceOf(Identifier::class.java) assertThat(tokens[1]).isInstanceOf(Identifier::class.java) @@ -180,8 +322,86 @@ class MathTokenizerTest { } @Test - fun testTokenize_multipleIdentifiers_withSpaces_producesIdentifierTokensForEachInOrder() { - val tokens = MathTokenizer.tokenize("x y z").toList() + fun testTokenize_multipleIdentifiers_multiLetter_withSpaces_producesIdTokensForEachInOrder() { + val tokens = MathTokenizer.tokenize("abc def", listOf("abc", "def")).toList() + + assertThat(tokens).hasSize(2) + assertThat(tokens[0]).isInstanceOf(Identifier::class.java) + assertThat(tokens[1]).isInstanceOf(Identifier::class.java) + assertThat((tokens[0] as Identifier).name).isEqualTo("abc") + assertThat((tokens[1] as Identifier).name).isEqualTo("def") + } + + @Test + fun testTokenize_multipleIdentifiers_mixed_withSpaces_producesIdTokensForEachInOrder() { + val tokens = MathTokenizer.tokenize("a lambda", listOf("a", "lambda")).toList() + + assertThat(tokens).hasSize(2) + assertThat(tokens[0]).isInstanceOf(Identifier::class.java) + assertThat(tokens[1]).isInstanceOf(Identifier::class.java) + assertThat((tokens[0] as Identifier).name).isEqualTo("a") + assertThat((tokens[1] as Identifier).name).isEqualTo("lambda") + } + + @Test + fun testTokenize_multiplyTwoVariables_singleLetter_producesCorrectTokens() { + val tokens = MathTokenizer.tokenize("x*y", ALLOWED_XYZ_VARIABLES).toList() + + assertThat(tokens).hasSize(3) + assertThat(tokens[0]).isInstanceOf(Identifier::class.java) + assertThat(tokens[1]).isInstanceOf(Operator::class.java) + assertThat(tokens[2]).isInstanceOf(Identifier::class.java) + assertThat((tokens[0] as Identifier).name).isEqualTo("x") + assertThat((tokens[1] as Operator).operator).isEqualTo('*') + assertThat((tokens[2] as Identifier).name).isEqualTo("y") + } + + @Test + fun testTokenize_multiplyTwoVariables_multiLetter_producesCorrectTokens() { + val tokens = MathTokenizer.tokenize("abc*def", listOf("abc", "def")).toList() + + assertThat(tokens).hasSize(3) + assertThat(tokens[0]).isInstanceOf(Identifier::class.java) + assertThat(tokens[1]).isInstanceOf(Operator::class.java) + assertThat(tokens[2]).isInstanceOf(Identifier::class.java) + assertThat((tokens[0] as Identifier).name).isEqualTo("abc") + assertThat((tokens[1] as Operator).operator).isEqualTo('*') + assertThat((tokens[2] as Identifier).name).isEqualTo("def") + } + + @Test + fun testTokenize_multipleMultiLetterVar_withConsecutiveSingleLetter_producesTokensForAllIds() { + val tokens = MathTokenizer.tokenize("lambda*xyz", ALLOWED_XYZ_WITH_LAMBDA_VARIABLES).toList() + + // The 'lambda' is a single variable, but the individual 'xyz' are separate variables that each + // show up separately (which allows interpreting implicit multiplication). + assertThat(tokens).hasSize(5) + assertThat(tokens[0]).isInstanceOf(Identifier::class.java) + assertThat(tokens[1]).isInstanceOf(Operator::class.java) + assertThat(tokens[2]).isInstanceOf(Identifier::class.java) + assertThat(tokens[3]).isInstanceOf(Identifier::class.java) + assertThat(tokens[4]).isInstanceOf(Identifier::class.java) + assertThat((tokens[0] as Identifier).name).isEqualTo("lambda") + assertThat((tokens[1] as Operator).operator).isEqualTo('*') + assertThat((tokens[2] as Identifier).name).isEqualTo("x") + assertThat((tokens[3] as Identifier).name).isEqualTo("y") + assertThat((tokens[4] as Identifier).name).isEqualTo("z") + } + + @Test + fun testTokenize_ambiguousMutliSingleLetterIds_producesIdForPreferredMultiLetterId() { + val tokens = MathTokenizer.tokenize("xyz", ALLOWED_XYZ_WITH_COMBINED_XYZ_VARIABLES).toList() + + // A single identifier should be parsed since the combined variable is encountered, and that + // takes precedent. + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isInstanceOf(Identifier::class.java) + assertThat((tokens[0] as Identifier).name).isEqualTo("xyz") + } + + @Test + fun testTokenize_ambiguousMutliSingleLetterIds_singleLetterIdsAlone_producesIdTokens() { + val tokens = MathTokenizer.tokenize("x y z", ALLOWED_XYZ_WITH_COMBINED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(3) assertThat(tokens[0]).isInstanceOf(Identifier::class.java) @@ -192,6 +412,42 @@ class MathTokenizerTest { assertThat((tokens[2] as Identifier).name).isEqualTo("z") } + @Test + fun testTokenize_ambiguousMutliSingleLetterIds_multiIdSubstring_producesIndividualIdTokens() { + val tokens = MathTokenizer.tokenize("yz", ALLOWED_XYZ_WITH_COMBINED_XYZ_VARIABLES).toList() + + // Partial substring of 'xyz' produces separate tokens since the whole token isn't present. + assertThat(tokens).hasSize(2) + assertThat(tokens[0]).isInstanceOf(Identifier::class.java) + assertThat(tokens[1]).isInstanceOf(Identifier::class.java) + assertThat((tokens[0] as Identifier).name).isEqualTo("y") + assertThat((tokens[1] as Identifier).name).isEqualTo("z") + } + + @Test + fun testTokenize_ambiguousMutliSingleLetterIds_outOfOrder_producesIdTokens() { + val tokens = MathTokenizer.tokenize("zyx", ALLOWED_XYZ_WITH_COMBINED_XYZ_VARIABLES).toList() + + // Reversing the tokens doesn't match the overall variable, so return separate variables. + assertThat(tokens).hasSize(3) + assertThat(tokens[0]).isInstanceOf(Identifier::class.java) + assertThat(tokens[1]).isInstanceOf(Identifier::class.java) + assertThat(tokens[2]).isInstanceOf(Identifier::class.java) + assertThat((tokens[0] as Identifier).name).isEqualTo("z") + assertThat((tokens[1] as Identifier).name).isEqualTo("y") + assertThat((tokens[2] as Identifier).name).isEqualTo("x") + } + + @Test + fun testTokenize_ambiguousMutliSingleLetterIds_multiWord_withInvalidLetter_producesInvalidId() { + val tokens = MathTokenizer.tokenize("xyzw", ALLOWED_XYZ_WITH_COMBINED_XYZ_VARIABLES).toList() + + // A single letter is sufficient to lead to an error ID. + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isInstanceOf(InvalidIdentifier::class.java) + assertThat((tokens[0] as InvalidIdentifier).name).isEqualTo("xyzw") + } + @Test fun testTokenize_identifier_whitespaceBefore_isIgnored() { val tokens = MathTokenizer.tokenize(" \r\t\n x").toList() @@ -283,4 +539,18 @@ class MathTokenizerTest { assertThat(tokens[14]).isInstanceOf(WholeNumber::class.java) assertThat((tokens[14] as WholeNumber).value).isEqualTo(3) } + + // TODO(#89): Move to a common test library. + private fun assertThrows(type: KClass, operation: () -> Unit): T { + try { + operation() + fail("Expected to encounter exception of $type") + } catch (t: Throwable) { + if (type.isInstance(t)) { + return type.cast(t) + } + // Unexpected exception; throw it. + throw t + } + } } From 8bfdd5dcf8207bae43cb3ab31ec413c2735817d1 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 10 Dec 2020 20:51:35 -0800 Subject: [PATCH 053/289] Fix & test math expression parsing. This fixes a few issues with the core parsing, largely refactors it to be more elegant, robust, and streamlined, adds documentation, adds significant testing, and properly adds two new features: unary negation and implied multiplication. All of the different feature aspects of expression parsing, the features above, and error cases are being thoroughly tested. None of the expression extention library or polynomial functionality is yet being tested. Finally, 'NEGATION' was renamed to 'NEGATE' in MathUnaryOperation to have parity with the operator names in MathBinaryOperation. --- model/src/main/proto/math.proto | 2 +- .../util/math/MathExpressionExtensions.kt | 4 +- .../android/util/math/MathExpressionParser.kt | 598 ++++++--- .../oppia/android/util/math/MathTokenizer.kt | 21 +- .../util/math/MathExpressionParserTest.kt | 1154 ++++++++++++++++- .../android/util/math/MathTokenizerTest.kt | 92 +- 6 files changed, 1649 insertions(+), 222 deletions(-) diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 73f23bd8c01..121b65430fd 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -48,7 +48,7 @@ message MathUnaryOperation { enum Operator { OPERATOR_UNKNOWN = 0; // Represents negating a value, e.g.: -y. - NEGATION = 1; + NEGATE = 1; } Operator operator = 1; diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index accc3c6660f..becdfd2dc6e 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -128,7 +128,7 @@ private fun MathExpression.reduceToPolynomial(): Polynomial? { private fun MathUnaryOperation.reduceToPolynomial(): Polynomial? { return when (operator) { - MathUnaryOperation.Operator.NEGATION -> -(operand.reduceToPolynomial() ?: return null) + MathUnaryOperation.Operator.NEGATE -> -(operand.reduceToPolynomial() ?: return null) else -> null } } @@ -387,7 +387,7 @@ private sealed class ExpressionTreeNode { //private fun MathUnaryOperation.reduceToConstant(): MathExpression? { // return when (operator) { -// MathUnaryOperation.Operator.NEGATION -> operand.reduceToConstant()?.transformConstant { -it } +// MathUnaryOperation.Operator.NEGATE -> operand.reduceToConstant()?.transformConstant { -it } // else -> null // } //} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index 193d423e93b..5b26ce42ce9 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -5,9 +5,9 @@ import org.oppia.android.app.model.MathBinaryOperation import org.oppia.android.app.model.MathExpression import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.Real -import org.oppia.android.util.math.MathTokenizer.Token.CloseParenthesis import org.oppia.android.util.math.MathTokenizer.Token.DecimalNumber import org.oppia.android.util.math.MathTokenizer.Token.Identifier +import org.oppia.android.util.math.MathTokenizer.Token.InvalidIdentifier import org.oppia.android.util.math.MathTokenizer.Token.InvalidToken import org.oppia.android.util.math.MathTokenizer.Token.OpenParenthesis import org.oppia.android.util.math.MathTokenizer.Token.Operator @@ -15,85 +15,115 @@ import org.oppia.android.util.math.MathTokenizer.Token.WholeNumber import java.util.ArrayDeque import java.util.Stack -private val OPERATOR_PRECEDENCES = mapOf('^' to 4, '*' to 3, '/' to 3, '+' to 2, '-' to 2) -private val LEFT_ASSOCIATIVE_OPERATORS = listOf('*', '/', '+', '-') - +/** + * Contains functionality for parsing mathematical expressions, including both numeric and + * polynomial-based algebraic expressions (functions are not currently supported). + */ class MathExpressionParser { companion object { + /** The result of parsing an expression. See the subclasses for the different possibilities. */ sealed class ParseResult { + /** Indicates a successful parse with a corresponding [MathExpression]. */ data class Success(val mathExpression: MathExpression) : ParseResult() + // TODO(BenHenning): Replace this with an enum so that UI code can show a reasonable error to + // the user. + /** Indicates the parse failed with a developer-readable string. */ data class Failure(val failureReason: String) : ParseResult() } - // TODO: update to support implied multiplication, e.g.: 2x^2. - fun parseExpression(literalExpression: String): ParseResult { + /** + * Parses the specified raw expression literal with the list of allowed variables and returns a + * [ParseResult] with either the parsed expression tree or a failure if the expression has an + * error. + * + * Note that this parsing will include some cases of implied multiplication. For example, each + * of the following cases will result in a valid parse with a multiplication operator despite + * one not being explicitly present in the expression: + * - 2x -> 2 * x (note that '2 x' will have the same result) + * - x2 -> x * 2 + * - (x+1)(x+2) -> (x+1) * (x+2) (note that whitespace between the groups is ignored) + * - xy -> x * y (note that 'x y' will have the same result) + * - x(x+1) -> x * (x+1) + * - 2(x+1) -> 2 * (x+1) + * - (x+1)x -> (x+1) * x + * - (x+1)2 -> (x+1) * 2 + * + * No other cases will result in implied multiplication (including '2 2' which is an invalid + * expression). However, sometimes these invalid cases can be made valid (e.g. (2)(3) should + * result in a proper, implied multiplication scenario since this is treated as polynomial + * multiplication). Note also that long variables (e.g. 'lambda' will be treated the same except + * they can never have implied multiplication with other variables since the parser cannot + * reasonably distinguish between different variables that are more than one letter long). + */ + fun parseExpression(literalExpression: String, allowedVariables: List): ParseResult { // An implementation of the Shunting Yard algorithm adapted to support variables, different - // number types, and the unary negation operator. References: + // number types, and the unary negate operator. References: // - https://en.wikipedia.org/wiki/Shunting-yard_algorithm#The_algorithm_in_detail // - https://wcipeg.com/wiki/Shunting_yard_algorithm#Unary_operators - val operatorStack = Stack() - val outputQueue = ArrayDeque() + val operatorStack = Stack() + val outputQueue = ArrayDeque() var lastToken: MathTokenizer.Token? = null - for (token in MathTokenizer.tokenize(literalExpression)) { - when (token) { - is WholeNumber, is DecimalNumber, is Identifier -> { - outputQueue += ParsedToken(token) - } - is Operator -> { - val precedence = token.getPrecedence() - ?: return ParseResult.Failure("Encountered unexpected operator: ${token.operator}") + for (token in tokenize(literalExpression, allowedVariables)) { + when (val parsedToken = ParsedToken.parseToken(token, lastToken)) { + is ParsedToken.Groupable.Computable.Operand -> outputQueue += parsedToken + is ParsedToken.Groupable.Computable.Operator -> { + val parsedOperator = parsedToken.parsedOperator + val precedence = parsedOperator.precedence + val isUnaryOperator = parsedOperator is ParsedOperator.UnaryOperator while (operatorStack.isNotEmpty()) { val top = operatorStack.peek() - if (top.token !is Operator) break - val topPrecedence = top.token.getPrecedence() - ?: return ParseResult.Failure( - "Encountered unexpected operator: ${top.token.operator}" - ) + if (top !is ParsedToken.Groupable.Computable.Operator) break + val topPrecedence = top.parsedOperator.precedence if (topPrecedence < precedence) break - if (topPrecedence == precedence && !token.isLeftAssociative()) break - outputQueue += operatorStack.pop() - } - // TODO: fix unary. - if (token.isMinusOperator() && lastToken.doesPreviousTokenIndicateNegation()) { - operatorStack.push(ParsedToken(token, isUnary = true)) - } else { - operatorStack.push(ParsedToken(token)) + if (isUnaryOperator) break // Unary operators do not pop operators. + if (topPrecedence == precedence + && parsedOperator is ParsedOperator.BinaryOperator + && parsedOperator.associativity != ParsedOperator.Associativity.LEFT) break + operatorStack.pop() + outputQueue += top } + operatorStack.push(parsedToken) } - is OpenParenthesis -> { - operatorStack.push(ParsedToken(token)) + is ParsedToken.Groupable.OpenParenthesis -> { + operatorStack.push(parsedToken) } - is CloseParenthesis -> { + is ParsedToken.CloseParenthesis -> { while (operatorStack.isNotEmpty()) { val top = operatorStack.peek() - if (top.token is OpenParenthesis) break - outputQueue += operatorStack.pop() + // The only non-computable, groupable token is OpenParenthesis. + if (top !is ParsedToken.Groupable.Computable) break + operatorStack.pop() + outputQueue += top } - if (operatorStack.isEmpty() || operatorStack.peek().token !is OpenParenthesis) { + if (operatorStack.isEmpty() + || operatorStack.peek() !is ParsedToken.Groupable.OpenParenthesis) { return ParseResult.Failure( - "Encountered unexpected close parenthesis at index ${token.column} in " + - token.source + "Encountered unexpected close parenthesis at index ${token.column} in " + + token.source ) } // Discard the open parenthesis since it's be finished. operatorStack.pop() } - is InvalidToken -> { - return ParseResult.Failure( - "Encountered unexpected symbol at index ${token.column} in ${token.source}" - ) - } + is ParsedToken.FailedToken -> return ParseResult.Failure(parsedToken.getFailureReason()) } lastToken = token } while (operatorStack.isNotEmpty()) { - when (val top = operatorStack.peek().token) { - is OpenParenthesis -> return ParseResult.Failure( - "Encountered unexpected close parenthesis at index ${top.column} in ${top.source}" - ) - else -> outputQueue += operatorStack.pop() + when (val top = operatorStack.peek()) { + // The only non-computable, groupable token is OpenParenthesis. + !is ParsedToken.Groupable.Computable -> { + val openParenthesis = top as ParsedToken.Groupable.OpenParenthesis + return ParseResult.Failure( + "Encountered unexpected open parenthesis at index ${openParenthesis.token.column}" + ) + } + else -> { + operatorStack.pop() + outputQueue += top + } } } @@ -101,136 +131,414 @@ class MathExpressionParser { // to avoid a second pass over the tokens (since then the expressions could be created // in-line). However, two passes is simpler (and by using postfix notation we can avoid // processing tokens that aren't needed if an error occurs during parsing). - val operandStack = Stack() + val operandStack = Stack() for (parsedToken in outputQueue) { - when (parsedToken.token) { - is WholeNumber, is DecimalNumber, is Identifier -> { - operandStack.push(TokenOrExpression.TokenWrapper(parsedToken.token)) - } - is Operator -> { - if (parsedToken.isUnary) { - if (parsedToken.token.operator != '-') { - return ParseResult.Failure( - "Encountered unexpected non-negation unary operator: " + - parsedToken.token.operator - ) + when (parsedToken) { + is ParsedToken.Groupable.Computable.Operand -> + operandStack.push(parsedToken.toMathExpression()) + is ParsedToken.Groupable.Computable.Operator -> when (parsedToken.parsedOperator) { + is ParsedOperator.UnaryOperator -> { + if (operandStack.isEmpty()) { + return ParseResult.Failure("Encountered unary operator without operand") + } + operandStack.push(parsedToken.parsedOperator.toMathExpression(operandStack.pop())) + } + is ParsedOperator.BinaryOperator -> { + if (operandStack.size < 2) { + return ParseResult.Failure("Encountered binary operator with missing operand(s)") } - val unaryOperationExpression = - MathExpression.newBuilder() - .setUnaryOperation( - MathUnaryOperation.newBuilder() - .setOperator(MathUnaryOperation.Operator.NEGATION) - .assignOperand(operandStack.pop(), MathUnaryOperation.Builder::setOperand) - ).build() - operandStack.push(TokenOrExpression.ExpressionWrapper(unaryOperationExpression)) - } else { val rightOperand = operandStack.pop() val leftOperand = operandStack.pop() - val operator = parseBinaryOperator(parsedToken.token) - ?: return ParseResult.Failure( - "Encountered unexpected binary operator: ${parsedToken.token.operator}" - ) - val binaryOperationExpression = - MathExpression.newBuilder() - .setBinaryOperation( - MathBinaryOperation.newBuilder() - .setOperator(operator) - .assignOperand(leftOperand, MathBinaryOperation.Builder::setLeftOperand) - .assignOperand(rightOperand, MathBinaryOperation.Builder::setRightOperand) - ).build() - operandStack.push(TokenOrExpression.ExpressionWrapper(binaryOperationExpression)) + operandStack.push( + parsedToken.parsedOperator.toMathExpression(leftOperand, rightOperand) + ) } } - else -> - return ParseResult.Failure( - "Encountered unexpected token during parsing: ${parsedToken.token}" - ) } } - val finalElement = operandStack.getFinalElement() - ?: return ParseResult.Failure("Failed to resolve expression tree: $operandStack") - return ParseResult.Success(finalElement.expression) + if (operandStack.size != 1) { + return ParseResult.Failure("Failed to resolve expression tree: $operandStack") + } + return ParseResult.Success(operandStack.firstElement()) } /** - * Returns the final element of the stack (which should only contain that element) which itself - * should be an expression, or null if something failed during parsing. + * Returns an iterable of tokens by tokenizing the provided expression & accounting for the list + * of allowed variables. This uses [MathTokenizer] & augments it by providing selective support + * for implied multiplication scenarios. */ - private fun Stack.getFinalElement(): TokenOrExpression.ExpressionWrapper? { - return if (size != 1 || firstElement() !is TokenOrExpression.ExpressionWrapper) null - else firstElement() as TokenOrExpression.ExpressionWrapper + private fun tokenize( + literalExpression: String, allowedVariables: List + ): Iterable { + return MathTokenizer.tokenize( + rawLiteral = literalExpression, allowedIdentifiers = allowedVariables + ).adaptTokenStreamForImpliedMultiplication() } - private fun B.assignOperand( - operand: TokenOrExpression, - setter: B.(MathExpression) -> B - ): B { - return when (operand) { - is TokenOrExpression.TokenWrapper -> this.setter(computeConstantOperand(operand.token)) - is TokenOrExpression.ExpressionWrapper -> this.setter(operand.expression) + /** + * Returns a new [Iterable] wrapped around the specified one with additional support for + * injecting synthesized tokens, as needed, into the token stream in order to support implied + * multiplication scenarios. See [ImpliedMultiplicationIteratorAdapter] for specifics on how + * this is implemented & the cases supported. + */ + private fun Iterable.adaptTokenStreamForImpliedMultiplication(): + Iterable { + val baseIterable = this + return object : Iterable { + override fun iterator(): Iterator = + ImpliedMultiplicationIteratorAdapter(baseIterable.iterator()) } } - private fun parseBinaryOperator(operator: Operator): MathBinaryOperation.Operator? { - return when (operator.operator) { - '+' -> MathBinaryOperation.Operator.ADD - '-' -> MathBinaryOperation.Operator.SUBTRACT - '*' -> MathBinaryOperation.Operator.MULTIPLY - '/' -> MathBinaryOperation.Operator.DIVIDE - '^' -> MathBinaryOperation.Operator.EXPONENTIATE - else -> null - } + /** + * Returns whether this token, as a previous token (potentially null for the first token of the + * stream) indicates that the token immediately following it could be a unary operator (if other + * sufficient conditions are met, such as the operator matches an expected unary operator + * symbol). + */ + private fun MathTokenizer.Token?.doesSuggestNegationInNextToken(): Boolean { + // A minus operator at the beginning of the stream, after a group is opened, and after + // another operator is always a unary negate operator. + return this == null || this is OpenParenthesis || this is Operator } - private fun computeConstantOperand(token: MathTokenizer.Token): MathExpression { - return when (token) { - is WholeNumber -> - MathExpression.newBuilder() - .setConstant( - Real.newBuilder().setRational( - Fraction.newBuilder().setWholeNumber(token.value).setDenominator(1) + /** + * Corresponds to an interpreted parsing of a mathematical token that can be analyzed and, in + * some cases, converted to a [MathExpression]. + * + * Note that this class & its subclasses heavily rely on various levels of sealed class + * inheritance to tighten the contracts around all types of tokens to facilitate writing highly + * robust code. See the separate classes to get an idea on how the structure is laid out. + */ + private sealed class ParsedToken { + companion object { + /** + * Returns a new [ParsedToken] for the specified token (& potentially considering the + * previous token), or null if the token corresponds to an operator that isn't recognized. + */ + fun parseToken(token: MathTokenizer.Token, lastToken: MathTokenizer.Token?) : ParsedToken? { + return when (token) { + is WholeNumber -> Groupable.Computable.Operand.WholeNumber(token.value) + is DecimalNumber -> Groupable.Computable.Operand.DecimalNumber(token.value) + is Identifier -> Groupable.Computable.Operand.Identifier(token.name) + is Operator -> + Groupable.Computable.Operator( + ParsedOperator.parseOperator(token, lastToken) + ?: return FailedToken.InvalidOperator(token.operator) ) - ).build() - is DecimalNumber -> - MathExpression.newBuilder() - .setConstant(Real.newBuilder().setIrrational(token.value)) - .build() - is Identifier -> MathExpression.newBuilder().setVariable(token.name).build() - else -> MathExpression.getDefaultInstance() // This case should never happen. + is OpenParenthesis -> Groupable.OpenParenthesis(token) + is MathTokenizer.Token.CloseParenthesis -> CloseParenthesis + is InvalidIdentifier -> FailedToken.InvalidIdentifier(token.name) + is InvalidToken -> FailedToken.InvalidToken(token) + } + } } - } - private sealed class TokenOrExpression { + /** + * Corresponds to a set of tokens that represent groupable components (e.g. open parenthesis, + * constants, etc.). Closed parenthesis is not included since that ends a group rather than + * begins/participates in one. + * + * This class exists to simplify error-handling in the shunting-yard algorithm. + */ + sealed class Groupable: ParsedToken() { + /** + * Corresponds to tokens that are computable (that is, can be converted to + * [MathExpression]s). + */ + sealed class Computable : Groupable() { + /** Corresponds to an operand for a unary or binary operation. */ + sealed class Operand : Computable() { + /** Returns a [MathExpression] representation of this operand. */ + abstract fun toMathExpression(): MathExpression + + /** An operand that is a whole number (e.g. '2'). */ + data class WholeNumber(private val value: Int) : Operand() { + override fun toMathExpression(): MathExpression = + MathExpression.newBuilder() + .setConstant( + Real.newBuilder().setRational( + Fraction.newBuilder().setWholeNumber(value).setDenominator(1) + ) + ).build() + } + + /** An operand that's a decimal (e.g. '3.14'). */ + data class DecimalNumber(private val value: Double) : Operand() { + override fun toMathExpression(): MathExpression = + MathExpression.newBuilder() + .setConstant(Real.newBuilder().setIrrational(value)) + .build() + } + + /** + * An operand that's an identifier (e.g. 'x') which will likely be treated as a + * variable. + */ + data class Identifier(private val name: String) : Operand() { + override fun toMathExpression(): MathExpression = + MathExpression.newBuilder().setVariable(name).build() + } + } + + /** + * Corresponds to an operator (binary or unary). See [ParsedOperator] for supported + * operators. + */ + data class Operator(val parsedOperator: ParsedOperator) : Computable() + } + + /** Corresponds to an open parenthesis token which begins a grouped expression. */ + data class OpenParenthesis(val token: MathTokenizer.Token) : Groupable() + } - data class TokenWrapper(val token: MathTokenizer.Token) : TokenOrExpression() + /** Corresponds to a close parenthesis token which ends a grouped expression. */ + object CloseParenthesis: ParsedToken() - data class ExpressionWrapper(val expression: MathExpression) : TokenOrExpression() + /** Corresponds to a token that represents a failure during tokenization or parsing. */ + sealed class FailedToken: ParsedToken() { + /** + * Returns the reason the failure token was created. This is not meant to be shown to end + * users, only developers. + */ + abstract fun getFailureReason(): String + + /** + * Indicates an invalid operator was encountered. This typically means the tokenizer + * supports operators that the parser does not. + */ + data class InvalidOperator(val operator: Char): FailedToken() { + override fun getFailureReason(): String = "Encountered unexpected operator: $operator" + } + + /** + * Indicates an identifier was encountered that doesn't correspond to any of the allowed + * variables passed to the parser during parsing time. + */ + data class InvalidIdentifier(val name: String): FailedToken() { + override fun getFailureReason(): String = "Encountered invalid identifier: $name" + } + + /** Indicates an invalid token was encountered during tokenization. */ + data class InvalidToken(private val token: MathTokenizer.Token): FailedToken() { + override fun getFailureReason(): String = + "Encountered unexpected symbol at index ${token.column} in ${token.source}" + } + } } - private data class ParsedToken(val token: MathTokenizer.Token, val isUnary: Boolean = false) + /** Corresponds to an operator parsed from an operator token with defined precedence. */ + private sealed class ParsedOperator(val precedence: Int) { + companion object { + /** + * Returns a new [ParsedOperator] given the specified [Operator], or null if the operator is + * not recognized. + * + * This uses the previous token to determine whether this operator is unary or binary. + */ + fun parseOperator(operator: Operator, lastToken: MathTokenizer.Token?): ParsedOperator? { + return when (operator.operator) { + '+' -> Add + '-' -> if (lastToken.doesSuggestNegationInNextToken()) Negate else Subtract + '*' -> Multiply + '/' -> Divide + '^' -> Exponentiate + else -> null + } + } + } + + /** + * Corresponds to relative associativity for other encounterd operations whose operators are + * at the same level of precedence. + */ + enum class Associativity { + LEFT, + RIGHT, + } + + /** Corresponds to a binary operation (e.g. 'x + y'). */ + abstract class BinaryOperator( + precedence: Int, + val associativity: Associativity, + private val protoOperator: MathBinaryOperation.Operator + ): ParsedOperator(precedence) { + /** Returns a [MathExpression] representation of this parsed operator. */ + fun toMathExpression( + leftOperand: MathExpression, + rightOperand: MathExpression + ): MathExpression = + MathExpression.newBuilder() + .setBinaryOperation( + MathBinaryOperation.newBuilder() + .setOperator(protoOperator) + .setLeftOperand(leftOperand) + .setRightOperand(rightOperand) + ).build() + } + + /** Corresponds to a unary operation (e.g. '-x'). */ + abstract class UnaryOperator( + precedence: Int, + private val protoOperator: MathUnaryOperation.Operator + ): ParsedOperator(precedence) { + /** Returns a [MathExpression] representation of this parsed operator. */ + fun toMathExpression(operand: MathExpression): MathExpression = + MathExpression.newBuilder() + .setUnaryOperation( + MathUnaryOperation.newBuilder() + .setOperator(protoOperator) + .setOperand(operand) + ).build() + } + + /** Corresponds to the addition operation, e.g.: 1 + 2. */ + object Add: BinaryOperator( + precedence = 1, + associativity = Associativity.LEFT, + protoOperator = MathBinaryOperation.Operator.ADD + ) + + /** Corresponds to the subtraction operation, e.g.: 1 - 2. */ + object Subtract: BinaryOperator( + precedence = Add.precedence, + associativity = Associativity.LEFT, + protoOperator = MathBinaryOperation.Operator.SUBTRACT + ) + + /** Corresponds to the multiplication operation, e.g.: 1 * 2. */ + object Multiply: BinaryOperator( + precedence = Add.precedence + 1, + associativity = Associativity.LEFT, + protoOperator = MathBinaryOperation.Operator.MULTIPLY + ) + + /** Corresponds to the division operation, e.g.: 1 / 2. */ + object Divide: BinaryOperator( + precedence = Multiply.precedence, + associativity = Associativity.LEFT, + protoOperator = MathBinaryOperation.Operator.DIVIDE + ) + + /** Corresponds to unary negation, e.g.: -1. */ + object Negate: UnaryOperator( + precedence = Multiply.precedence + 1, + protoOperator = MathUnaryOperation.Operator.NEGATE + ) + + /** Corresponds to the exponentiation operation, e.g.: 1 ^ 2. */ + object Exponentiate: BinaryOperator( + precedence = Negate.precedence + 1, + associativity = Associativity.RIGHT, + protoOperator = MathBinaryOperation.Operator.EXPONENTIATE + ) + } /** - * Returns whether this token, as a previous token (potentially null for the first token of the - * stream) indicates that the token immediately following it could be a unary negation operator - * (if it's the minus operator). + * An adapter of the iterator returned by [MathTokenizer] that will synthesize operators in + * cases when there's implied multiplication (e.g. 2xy should be interpreted as 2*x*y). */ - private fun MathTokenizer.Token?.doesPreviousTokenIndicateNegation(): Boolean { - // A minus operator at the beginning of the stream, after a group is opened, and after - // another operator is always a unary negation operator. - return this == null || this is OpenParenthesis || this is Operator - } + private class ImpliedMultiplicationIteratorAdapter( + private val baseIterator: Iterator + ): Iterator { + private var lastToken: MathTokenizer.Token? = null + private var nextToken: MathTokenizer.Token? = null - private fun Operator.isMinusOperator(): Boolean { - return operator == '-' - } + // The base iterator's hasNext() is sufficient since this adapter will only ever synthesize + // new tokens *before* another token (since the synthesized tokens are always binary + // operators), except in the end-of-stream case (since the adapter might have a next token + // saved). + override fun hasNext(): Boolean = baseIterator.hasNext() || nextToken != null - private fun Operator.getPrecedence(): Int? { - return OPERATOR_PRECEDENCES[operator] - } + override fun next(): MathTokenizer.Token { + val (currentToken, newNextToken) = + computeCurrentTokenState(lastToken, nextToken ?: baseIterator.next()) + nextToken = newNextToken + lastToken = currentToken + return currentToken + } + + /** + * Returns a destructible data object of two elements defining first, the next token to + * return, and second, the token that should be used the next time next() is called, or null + * if none should be used (meaning a new element should be retrieved from the backing iterator + * on the next call to next()). + * + * @param lastToken the previous token provided via next(), or null if none + * @param nextToken the next token that should be provided to the user + */ + private fun computeCurrentTokenState( + lastToken: MathTokenizer.Token?, + nextToken: MathTokenizer.Token + ): NewTokenState { + return when { + lastToken.impliesMultiplicationWith(nextToken) -> NewTokenState( + currentToken = synthesizeMultiplicationOperatorToken(), nextToken = nextToken + ) + else -> NewTokenState(currentToken = nextToken, nextToken = null) + } + } + + /** + * Returns a new multiplication operator token to enable the parser to imply multiplication in + * certain contexts. + */ + private fun synthesizeMultiplicationOperatorToken(): MathTokenizer.Token { + return Operator(source = "", column = 0, operator = '*') + } + + /** Returns whether this token is constant (e.g. a whole number or variable). */ + private fun MathTokenizer.Token.isConstant(): Boolean { + return this is WholeNumber || this is DecimalNumber + } + + /** + * Returns whether this token is a variable (which, at the moment, is determined based on + * whether it's an identifier since identifiers aren't currently used for any other purpose). + */ + private fun MathTokenizer.Token.isVariable(): Boolean { + return this is Identifier + } + + /** + * Returns whether this token is a variable or constant (see the corresponding functions above + * for specifics on what each means in practice). + */ + private fun MathTokenizer.Token.isVariableOrConstant(): Boolean { + return this.isVariable() || this.isConstant() + } + + /** + * Returns whether this token, in conjunction with the specified token, indicates a scenario + * where multiplication should be implied. See the implementation for specifics. + */ + private fun MathTokenizer.Token?.impliesMultiplicationWith( + nextToken: MathTokenizer.Token + ): Boolean { + // Two consecutive tokens imply multiplication iff they are both variables, or one is a + // variable and the other is a constant. Or, a variable/constant is followed by an open + // parenthesis or a close parenthesis is followed by a variable/constant. Finally, two + // consecutive sets of parentheses also imply multiplication. + return when { + this == null -> false + this.isVariable() && nextToken.isVariable() -> true + this.isConstant() && nextToken.isVariable() -> true + this.isVariable() && nextToken.isConstant() -> true + this.isVariableOrConstant() && nextToken is OpenParenthesis -> true + this is MathTokenizer.Token.CloseParenthesis && nextToken.isVariableOrConstant() -> true + this is MathTokenizer.Token.CloseParenthesis && nextToken is OpenParenthesis -> true + else -> false + } + } - private fun Operator.isLeftAssociative(): Boolean { - return operator in LEFT_ASSOCIATIVE_OPERATORS + /** + * Temporary data object to signal to the adapter which token to return to the parser & which, + * if any, to cache for future calls to the iterator. + */ + private data class NewTokenState( + val currentToken: MathTokenizer.Token, val nextToken: MathTokenizer.Token? + ) } } } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt index 339a5153e2d..0cf43e850bf 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt @@ -118,27 +118,32 @@ class MathTokenizer { * 'i', and 'pi', then encounters of 'pi' will use the multi-letter identifier rather than p*i. * * @param allowedIdentifiers a list of acceptable identifiers that can be parsed (these may be - * more than one letter long). By default, the identifiers 'x' and 'y' are used. Any - * identifiers encountered that aren't part of this list will result in an invalid - * identifier token being returned. Note that identifiers must only contain strings with - * letters (per the definition of Character.isLetter()). This list can be empty (in which - * case all encountered identifiers will be presumed invalid). + * more than one letter long). Any identifiers encountered that aren't part of this list + * will result in an invalid identifier token being returned. Note that identifiers must + * only contain strings with letters (per the definition of Character.isLetter()). This list + * can be empty (in which case all encountered identifiers will be presumed invalid). */ fun tokenize( rawLiteral: String, - allowedIdentifiers: List = listOf("x", "y") + allowedIdentifiers: List ): Iterable { // Verify that the provided identifiers are all valid. for (identifier in allowedIdentifiers) { if (identifier.any(Char::isNotLetter)) { throw IllegalArgumentException("Identifier contains non-letters: $identifier") } + if (identifier.isEmpty()) { + throw IllegalArgumentException("Encountered empty identifier in allowed identifier list") + } } val lowercaseLiteral = rawLiteral.toLowerCase(Locale.getDefault()) - val lowercaseIdentifiers = allowedIdentifiers.map { it.toLowerCase(Locale.getDefault()) } + val lowercaseIdentifiers = allowedIdentifiers.map { + it.toLowerCase(Locale.getDefault()) + }.toSet() return object : Iterable { - override fun iterator(): Iterator = Tokenizer(lowercaseLiteral, lowercaseIdentifiers) + override fun iterator(): Iterator = + Tokenizer(lowercaseLiteral, lowercaseIdentifiers.toList()) } } diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index 4240a6c5634..70763aa6fee 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -5,10 +5,30 @@ import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.Fraction +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.Real +import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL +import org.oppia.android.util.math.MathExpressionParser.Companion.ParseResult +import org.oppia.android.util.math.MathExpressionParser.Companion.ParseResult.Failure import org.oppia.android.util.math.MathExpressionParser.Companion.ParseResult.Success import org.robolectric.annotation.LooperMode +import kotlin.reflect.KClass +import kotlin.reflect.full.cast +import kotlin.test.fail /** Tests for [MathExpressionParser]. */ @RunWith(AndroidJUnit4::class) @@ -18,26 +38,22 @@ class MathExpressionParserTest { // fun testParse_emptyString_returnsFailure() { // } - // test different parenthesis errors - // test unary operators - // test nested parenthesis - // test associativity (left & right) - // test order of operations - // test multiple variables - // test reals vs. whole numbers - // test nested operations for each operator + // test various incorrect formatting errors including: + // - invalid variables + // - incorrect parentheses + // - consecutive binary operators + // - multiple expressions at the root // TODO: add support for implied multiplication (e.g. 'xy' should imply x*y). Ditto for coefficients. + // TOOD: test decimals, long variables, etc. - @Test - fun testParse_constantNumber_returnsExpressionWithFractionWholeNumber() { // val result = MathExpressionParser.parseExpression("1") - //val result = MathExpressionParser.parseExpression("133 + 3.14 * x / (11 - 15) ^ 2 ^ 3") + // val result = MathExpressionParser.parseExpression("133 + 3.14 * x / (11 - 15) ^ 2 ^ 3") // val result = MathExpressionParser.parseExpression("3 + 4 * 2 / (1 - 5) ^ 2 ^ 3") - // Test: 10/-1*-2 to verify unary precedence. - // Test unary at: beginning, after open paren, close paren (should be minus), and after operators - // Test multiple (2 & 3) unaries after each of the above - // Test invalid operator & operand cases, e.g. multiple operands or operators in wrong place + // Test: 10/-1*-2 to verify unary precedence. + // Test unary at: beginning, after open paren, close paren (should be minus), and after operators + // Test multiple (2 & 3) unaries after each of the above + // Test invalid operator & operand cases, e.g. multiple operands or operators in wrong place // val result = MathExpressionParser.parseExpression("x^2*y^2 + 2") // works // val result = MathExpressionParser.parseExpression("(x-1)^3") // works @@ -45,22 +61,1110 @@ class MathExpressionParserTest { // val result = MathExpressionParser.parseExpression("x^2-3*x-10") // works // val result = MathExpressionParser.parseExpression("x+2") // works // val result = MathExpressionParser.parseExpression("4*(x+2)") // works - val result = MathExpressionParser.parseExpression("(x^2-3*x-10)*(x+2)") // works +// val result = MathExpressionParser.parseExpression("(x^2-3*x-10)*(x+2)") // works // val result = MathExpressionParser.parseExpression("(x^2-3*x-10)/(x+2)") // fails - val polynomial = (result as Success).mathExpression.toPolynomial() +// val polynomial = (result as Success).mathExpression.toPolynomial() + +// println("@@@@@ Result: ${(result as Success).mathExpression}}") +// println("@@@@@ Polynomial: $polynomial") +// println("@@@@@ Polynomial str: ${polynomial?.toAnswerString()}") + +// assertThat(result).isInstanceOf(Success::class.java) +// val expression = (result as Success).mathExpression +// assertThat(expression.expressionTypeCase).isEqualTo(MathExpression.ExpressionTypeCase.CONSTANT) +// assertThat(expression.constant.realTypeCase).isEqualTo(Real.RealTypeCase.RATIONAL) +// assertThat(expression.constant.rational).isEqualTo(createWholeNumberFraction(1)) + + @Test + fun testParse_constantWholeNumber_parsesSuccessfully() { + val result = MathExpressionParser.parseExpression("1", allowedVariables = listOf()) - println("@@@@@ Result: ${(result as Success).mathExpression}}") - println("@@@@@ Polynomial: $polynomial") - println("@@@@@ Polynomial str: ${polynomial?.toAnswerString()}") + assertThat(result).isInstanceOf(Success::class.java) + } + @Test + fun testParse_constantWholeNumber_returnsExpressionWithFractionWholeNumber() { + val result = MathExpressionParser.parseExpression("1", allowedVariables = listOf()) + + val rootExpression = result.getExpectedSuccessfulExpression() assertThat(result).isInstanceOf(Success::class.java) - val expression = (result as Success).mathExpression - assertThat(expression.expressionTypeCase).isEqualTo(MathExpression.ExpressionTypeCase.CONSTANT) - assertThat(expression.constant.realTypeCase).isEqualTo(Real.RealTypeCase.RATIONAL) - assertThat(expression.constant.rational).isEqualTo(createWholeNumberFraction(1)) + assertThat(rootExpression.constant.realTypeCase).isEqualTo(RATIONAL) + assertThat(rootExpression.constant.rational).isEqualTo(createWholeNumberFraction(1)) + } + + @Test + fun testParse_constantDecimalNumber_returnsExpressionWithIrrationalNumber() { + val result = MathExpressionParser.parseExpression("3.14", allowedVariables = listOf()) + + val rootExpression = result.getExpectedSuccessfulExpression() + assertThat(rootExpression.expressionTypeCase).isEqualTo(CONSTANT) + assertThat(rootExpression.constant.realTypeCase).isEqualTo(IRRATIONAL) + assertThat(rootExpression.constant.irrational).isWithin(1e-5).of(3.14) + } + + @Test + fun testParse_variable_returnsExpressionWithVariable() { + val result = MathExpressionParser.parseExpression("x", allowedVariables = listOf("x")) + + val rootExpression = result.getExpectedSuccessfulExpression() + assertThat(rootExpression.expressionTypeCase).isEqualTo(VARIABLE) + assertThat(rootExpression.variable).isEqualTo("x") + } + + @Test + fun testParse_multipleShortVariables_returnsExpressionWithBothVariables() { + val result = MathExpressionParser.parseExpression("x+y", allowedVariables = listOf("x", "y")) + + val rootExpression = result.getExpectedSuccessfulExpression() + val binOp = rootExpression.getExpectedBinaryOperation() + val leftVar = binOp.leftOperand.getExpectedVariable() + val rightVar = binOp.rightOperand.getExpectedVariable() + assertThat(leftVar).isEqualTo("x") + assertThat(rightVar).isEqualTo("y") + } + + @Test + fun testParse_longVariable_returnsExpressionWithVariable() { + val result = + MathExpressionParser.parseExpression("1+lambda", allowedVariables = listOf("lambda")) + + val rootExpression = result.getExpectedSuccessfulExpression() + val binOp = rootExpression.getExpectedBinaryOperation() + val rightVar = binOp.rightOperand.getExpectedVariable() + assertThat(rightVar).isEqualTo("lambda") + } + + @Test + fun testParse_mixedLongAndShortVariable_returnsExpressionWithBothVariables() { + val result = + MathExpressionParser.parseExpression("lambda+y", allowedVariables = listOf("y", "lambda")) + + val rootExpression = result.getExpectedSuccessfulExpression() + val binOp = rootExpression.getExpectedBinaryOperation() + val leftVar = binOp.leftOperand.getExpectedVariable() + val rightVar = binOp.rightOperand.getExpectedVariable() + assertThat(leftVar).isEqualTo("lambda") + assertThat(rightVar).isEqualTo("y") + } + + @Test + fun testParse_negation_returnsUnaryExpressionNegatingVariable() { + val result = MathExpressionParser.parseExpression("-x", allowedVariables = listOf("x")) + + val rootExpression = result.getExpectedSuccessfulExpression() + val unaryOp = rootExpression.getExpectedUnaryOperation() + val variableOperand = unaryOp.operand.getExpectedVariable() + assertThat(unaryOp.operator).isEqualTo(NEGATE) + assertThat(variableOperand).isEqualTo("x") + } + + @Test + fun testParse_negateExpression_returnsUnaryExpressionBeingNegated() { + val result = MathExpressionParser.parseExpression("-(x+2)", allowedVariables = listOf("x")) + + val rootExpression = result.getExpectedSuccessfulExpression() + val unaryOp = rootExpression.getExpectedUnaryOperationWithOperator(NEGATE) + // The entire operation should be negated. + assertThat(unaryOp.operand.expressionTypeCase).isEqualTo(BINARY_OPERATION) + } + + @Test + fun testParse_doubleNegate_cascadesInTree() { + val result = MathExpressionParser.parseExpression("--x", allowedVariables = listOf("x")) + + // Expected tree (left-to-right): negate(negate(x)) + val rootExpression = result.getExpectedSuccessfulExpression() + val outerOp = rootExpression.getExpectedUnaryOperationWithOperator(NEGATE) + val innerOp = outerOp.operand.getExpectedUnaryOperationWithOperator(NEGATE) + assertThat(innerOp.operand.expressionTypeCase).isEqualTo(VARIABLE) + } + + @Test + fun testParse_addVariableAndConstant_returnsBinaryExpression() { + val result = MathExpressionParser.parseExpression("x+2", allowedVariables = listOf("x")) + + val rootExpression = result.getExpectedSuccessfulExpression() + val binaryOp = rootExpression.getExpectedBinaryOperation() + val leftVariable = binaryOp.leftOperand.getExpectedVariable() + val rightConstant = binaryOp.rightOperand.getExpectedConstant() + assertThat(binaryOp.operator).isEqualTo(ADD) + assertThat(leftVariable).isEqualTo("x") + assertThat(rightConstant.realTypeCase).isEqualTo(RATIONAL) + assertThat(rightConstant.rational).isEqualTo(createWholeNumberFraction(2)) + } + + @Test + fun testParse_addVariableAndIrrationalConstant_returnsBinaryExpression() { + val result = MathExpressionParser.parseExpression("x+2.718", allowedVariables = listOf("x")) + + val rootExpression = result.getExpectedSuccessfulExpression() + val binaryOp = rootExpression.getExpectedBinaryOperation() + val leftVariable = binaryOp.leftOperand.getExpectedVariable() + val rightConstant = binaryOp.rightOperand.getExpectedConstant() + assertThat(binaryOp.operator).isEqualTo(ADD) + assertThat(leftVariable).isEqualTo("x") + assertThat(rightConstant.realTypeCase).isEqualTo(IRRATIONAL) + assertThat(rightConstant.irrational).isWithin(1e-5).of(2.718) + } + + @Test + fun testParse_addConstantWithNegatedVariable_combinesUnaryAndBinaryOperations() { + val result = MathExpressionParser.parseExpression("2+-x", allowedVariables = listOf("x")) + + // Expected tree (left-to-right): add(2, negate(x)) + val rootExpression = result.getExpectedSuccessfulExpression() + val binaryOp = rootExpression.getExpectedBinaryOperationWithOperator(ADD) + val leftConstant = binaryOp.leftOperand.getExpectedRationalConstant() + val rightNegateOp = binaryOp.rightOperand.getExpectedUnaryOperationWithOperator(NEGATE) + val negatedVariable = rightNegateOp.operand.getExpectedVariable() + assertThat(leftConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(negatedVariable).isEqualTo("x") + } + + @Test + fun testParse_subtractVariableAndConstant_returnsBinaryExpression() { + val result = MathExpressionParser.parseExpression("x-2", allowedVariables = listOf("x")) + + val rootExpression = result.getExpectedSuccessfulExpression() + val binaryOp = rootExpression.getExpectedBinaryOperation() + val leftVariable = binaryOp.leftOperand.getExpectedVariable() + val rightConstant = binaryOp.rightOperand.getExpectedConstant() + assertThat(binaryOp.operator).isEqualTo(SUBTRACT) + assertThat(leftVariable).isEqualTo("x") + assertThat(rightConstant.realTypeCase).isEqualTo(RATIONAL) + assertThat(rightConstant.rational).isEqualTo(createWholeNumberFraction(2)) + } + + @Test + fun testParse_multiplyVariableAndConstant_returnsBinaryExpression() { + val result = MathExpressionParser.parseExpression("x × 2", allowedVariables = listOf("x")) + + val rootExpression = result.getExpectedSuccessfulExpression() + val binaryOp = rootExpression.getExpectedBinaryOperation() + val leftVariable = binaryOp.leftOperand.getExpectedVariable() + val rightConstant = binaryOp.rightOperand.getExpectedConstant() + assertThat(binaryOp.operator).isEqualTo(MULTIPLY) + assertThat(leftVariable).isEqualTo("x") + assertThat(rightConstant.realTypeCase).isEqualTo(RATIONAL) + assertThat(rightConstant.rational).isEqualTo(createWholeNumberFraction(2)) + } + + @Test + fun testParse_divideVariableAndConstant_returnsBinaryExpression() { + val result = MathExpressionParser.parseExpression("x ÷ 2", allowedVariables = listOf("x")) + + val rootExpression = result.getExpectedSuccessfulExpression() + val binaryOp = rootExpression.getExpectedBinaryOperation() + val leftVariable = binaryOp.leftOperand.getExpectedVariable() + val rightConstant = binaryOp.rightOperand.getExpectedConstant() + assertThat(binaryOp.operator).isEqualTo(DIVIDE) + assertThat(leftVariable).isEqualTo("x") + assertThat(rightConstant.realTypeCase).isEqualTo(RATIONAL) + assertThat(rightConstant.rational).isEqualTo(createWholeNumberFraction(2)) + } + + @Test + fun testParse_variableRaisedByConstant_returnsBinaryExpression() { + val result = MathExpressionParser.parseExpression("x^2", allowedVariables = listOf("x")) + + val rootExpression = result.getExpectedSuccessfulExpression() + val binaryOp = rootExpression.getExpectedBinaryOperation() + val leftVariable = binaryOp.leftOperand.getExpectedVariable() + val rightConstant = binaryOp.rightOperand.getExpectedConstant() + assertThat(binaryOp.operator).isEqualTo(EXPONENTIATE) + assertThat(leftVariable).isEqualTo("x") + assertThat(rightConstant.realTypeCase).isEqualTo(RATIONAL) + assertThat(rightConstant.rational).isEqualTo(createWholeNumberFraction(2)) + } + + @Test + fun testParse_binaryAddition_withRedundantParentheses_returnsSameBinaryExpression() { + // Test that extra parentheses don't change the result. + val result = MathExpressionParser.parseExpression("(x+2)", allowedVariables = listOf("x")) + + val rootExpression = result.getExpectedSuccessfulExpression() + val binaryOp = rootExpression.getExpectedBinaryOperation() + val leftVariable = binaryOp.leftOperand.getExpectedVariable() + val rightConstant = binaryOp.rightOperand.getExpectedConstant() + assertThat(binaryOp.operator).isEqualTo(ADD) + assertThat(leftVariable).isEqualTo("x") + assertThat(rightConstant.realTypeCase).isEqualTo(RATIONAL) + assertThat(rightConstant.rational).isEqualTo(createWholeNumberFraction(2)) + } + + @Test + fun testParse_twoAdditions_haveLeftToRightAssociativity() { + val result = MathExpressionParser.parseExpression("1+3+2", allowedVariables = listOf()) + + // Left-to-right associativity means the first encountered addition is done first, and the + // second is done last (which means it's at the root). Expect the following tree: + // + + // + 2 + // 1 3 + val rootExpression = result.getExpectedSuccessfulExpression() + val rootAddOp = rootExpression.getExpectedBinaryOperationWithOperator(ADD) + val rootLeftAddOp = rootAddOp.leftOperand.getExpectedBinaryOperationWithOperator(ADD) + val rootRightConstant = rootAddOp.rightOperand.getExpectedRationalConstant() + val innerOpLeftConstant = rootLeftAddOp.leftOperand.getExpectedRationalConstant() + val innerOpRightConstant = rootLeftAddOp.rightOperand.getExpectedRationalConstant() + assertThat(rootRightConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(1)) + assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(3)) + } + + @Test + fun testParse_additionThenSubtraction_haveLeftToRightAssociativity() { + val result = MathExpressionParser.parseExpression("1+3-2", allowedVariables = listOf()) + + // Left-to-right associativity means the first encountered addition is done first, and the + // second is done last (which means it's at the root). Expect the following tree: + // - + // + 2 + // 1 3 + val rootExpression = result.getExpectedSuccessfulExpression() + val rootSubOp = rootExpression.getExpectedBinaryOperationWithOperator(SUBTRACT) + val rootLeftAddOp = rootSubOp.leftOperand.getExpectedBinaryOperationWithOperator(ADD) + val rootRightConstant = rootSubOp.rightOperand.getExpectedRationalConstant() + val innerOpLeftConstant = rootLeftAddOp.leftOperand.getExpectedRationalConstant() + val innerOpRightConstant = rootLeftAddOp.rightOperand.getExpectedRationalConstant() + assertThat(rootRightConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(1)) + assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(3)) + } + + @Test + fun testParse_subtractionThenAddition_haveLeftToRightAssociativity() { + val result = MathExpressionParser.parseExpression("1-3+2", allowedVariables = listOf()) + + // Left-to-right associativity means the first encountered addition is done first, and the + // second is done last (which means it's at the root). Expect the following tree: + // + + // - 2 + // 1 3 + val rootExpression = result.getExpectedSuccessfulExpression() + val rootAddOp = rootExpression.getExpectedBinaryOperationWithOperator(ADD) + val rootLeftSubOp = rootAddOp.leftOperand.getExpectedBinaryOperationWithOperator(SUBTRACT) + val rootRightConstant = rootAddOp.rightOperand.getExpectedRationalConstant() + val innerOpLeftConstant = rootLeftSubOp.leftOperand.getExpectedRationalConstant() + val innerOpRightConstant = rootLeftSubOp.rightOperand.getExpectedRationalConstant() + assertThat(rootRightConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(1)) + assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(3)) + } + + @Test + fun testParse_twoMultiplies_haveLeftToRightAssociativity() { + val result = MathExpressionParser.parseExpression("1*3*2", allowedVariables = listOf()) + + // Left-to-right associativity means the first encountered binary op is done first, and the + // second is done last (which means it's at the root). Expect the following tree: + // * + // * 2 + // 1 3 + val rootExpression = result.getExpectedSuccessfulExpression() + val rootMulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val rootLeftMulOp = rootMulOp.leftOperand.getExpectedBinaryOperationWithOperator(MULTIPLY) + val rootRightConstant = rootMulOp.rightOperand.getExpectedRationalConstant() + val innerOpLeftConstant = rootLeftMulOp.leftOperand.getExpectedRationalConstant() + val innerOpRightConstant = rootLeftMulOp.rightOperand.getExpectedRationalConstant() + assertThat(rootRightConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(1)) + assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(3)) + } + + @Test + fun testParse_multiplyThenDivide_haveLeftToRightAssociativity() { + val result = MathExpressionParser.parseExpression("1*3/2", allowedVariables = listOf()) + + // Left-to-right associativity means the first encountered binary op is done first, and the + // second is done last (which means it's at the root). Expect the following tree: + // / + // * 2 + // 1 3 + val rootExpression = result.getExpectedSuccessfulExpression() + val rootDivOp = rootExpression.getExpectedBinaryOperationWithOperator(DIVIDE) + val rootLeftMulOp = rootDivOp.leftOperand.getExpectedBinaryOperationWithOperator(MULTIPLY) + val rootRightConstant = rootDivOp.rightOperand.getExpectedRationalConstant() + val innerOpLeftConstant = rootLeftMulOp.leftOperand.getExpectedRationalConstant() + val innerOpRightConstant = rootLeftMulOp.rightOperand.getExpectedRationalConstant() + assertThat(rootRightConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(1)) + assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(3)) + } + + @Test + fun testParse_divideThenMultiply_haveLeftToRightAssociativity() { + val result = MathExpressionParser.parseExpression("1/3*2", allowedVariables = listOf()) + + // Left-to-right associativity means the first encountered binary op is done first, and the + // second is done last (which means it's at the root). Expect the following tree: + // * + // / 2 + // 1 3 + val rootExpression = result.getExpectedSuccessfulExpression() + val rootMulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val rootLeftDivOp = rootMulOp.leftOperand.getExpectedBinaryOperationWithOperator(DIVIDE) + val rootRightConstant = rootMulOp.rightOperand.getExpectedRationalConstant() + val innerOpLeftConstant = rootLeftDivOp.leftOperand.getExpectedRationalConstant() + val innerOpRightConstant = rootLeftDivOp.rightOperand.getExpectedRationalConstant() + assertThat(rootRightConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(1)) + assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(3)) + } + + @Test + fun testParse_twoExponentiations_haveRightToLeftAssociativity() { + val result = MathExpressionParser.parseExpression("1^3^2", allowedVariables = listOf()) + + // Right-to-left associativity means the opposite of left-to-right: always perform operations in + // the opposite order encountered. Expect the following tree: + // ^ + // 1 ^ + // 3 2 + val rootExpression = result.getExpectedSuccessfulExpression() + val rootExpOp = rootExpression.getExpectedBinaryOperationWithOperator(EXPONENTIATE) + val rootLeftConstant = rootExpOp.leftOperand.getExpectedRationalConstant() + val rootRightExpOp = rootExpOp.rightOperand.getExpectedBinaryOperationWithOperator(EXPONENTIATE) + val innerOpLeftConstant = rootRightExpOp.leftOperand.getExpectedRationalConstant() + val innerOpRightConstant = rootRightExpOp.rightOperand.getExpectedRationalConstant() + assertThat(rootLeftConstant).isEqualTo(createWholeNumberFraction(1)) + assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(3)) + assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(2)) + } + + @Test + fun testParse_addThenMultiply_multiplyHappensFirst() { + val result = MathExpressionParser.parseExpression("1+3*2", allowedVariables = listOf()) + + // Operator precedence means ensure multiplication happens first, but keep the general order + // of operands. Expect the following tree: + // + + // 1 * + // 3 2 + val rootExpression = result.getExpectedSuccessfulExpression() + val rootAddOp = rootExpression.getExpectedBinaryOperationWithOperator(ADD) + val rootLeftConstant = rootAddOp.leftOperand.getExpectedRationalConstant() + val rootRightMulOp = rootAddOp.rightOperand.getExpectedBinaryOperationWithOperator(MULTIPLY) + val innerOpLeftConstant = rootRightMulOp.leftOperand.getExpectedRationalConstant() + val innerOpRightConstant = rootRightMulOp.rightOperand.getExpectedRationalConstant() + assertThat(rootLeftConstant).isEqualTo(createWholeNumberFraction(1)) + assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(3)) + assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(2)) + } + + @Test + fun testParse_multiplyThenAdd_multiplyHappensFirst() { + val result = MathExpressionParser.parseExpression("1*3+2", allowedVariables = listOf()) + + // Operator precedence follows expression order. Expect the following tree: + // + + // * 2 + // 1 3 + val rootExpression = result.getExpectedSuccessfulExpression() + val rootAddOp = rootExpression.getExpectedBinaryOperationWithOperator(ADD) + val rootLeftMulOp = rootAddOp.leftOperand.getExpectedBinaryOperationWithOperator(MULTIPLY) + val rootRightConstant = rootAddOp.rightOperand.getExpectedRationalConstant() + val innerOpLeftConstant = rootLeftMulOp.leftOperand.getExpectedRationalConstant() + val innerOpRightConstant = rootLeftMulOp.rightOperand.getExpectedRationalConstant() + assertThat(rootRightConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(1)) + assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(3)) + } + + @Test + fun testParse_multiplyThenExponentaite_exponentiationHappensFirst() { + val result = MathExpressionParser.parseExpression("1*3^2", allowedVariables = listOf()) + + // Operator precedence means ensure exponentiation happens first, but keep the general order + // of operands. Expect the following tree: + // * + // 1 ^ + // 3 2 + val rootExpression = result.getExpectedSuccessfulExpression() + val rootMulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val rootLeftConstant = rootMulOp.leftOperand.getExpectedRationalConstant() + val rootRightExpOp = rootMulOp.rightOperand.getExpectedBinaryOperationWithOperator(EXPONENTIATE) + val innerOpLeftConstant = rootRightExpOp.leftOperand.getExpectedRationalConstant() + val innerOpRightConstant = rootRightExpOp.rightOperand.getExpectedRationalConstant() + assertThat(rootLeftConstant).isEqualTo(createWholeNumberFraction(1)) + assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(3)) + assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(2)) + } + + @Test + fun testParse_exponentiateThenMultiply_exponentiationHappensFirst() { + val result = MathExpressionParser.parseExpression("1^3*2", allowedVariables = listOf()) + + // Operator precedence follows expression order. Expect the following tree: + // * + // ^ 2 + // 1 3 + val rootExpression = result.getExpectedSuccessfulExpression() + val rootMulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val rootLeftExpOp = rootMulOp.leftOperand.getExpectedBinaryOperationWithOperator(EXPONENTIATE) + val rootRightConstant = rootMulOp.rightOperand.getExpectedRationalConstant() + val innerOpLeftConstant = rootLeftExpOp.leftOperand.getExpectedRationalConstant() + val innerOpRightConstant = rootLeftExpOp.rightOperand.getExpectedRationalConstant() + assertThat(rootRightConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(1)) + assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(3)) + } + + @Test + fun testParse_addThenMultiply_withParentheses_addHappensFirst() { + val result = MathExpressionParser.parseExpression("(1+3)*2", allowedVariables = listOf()) + + // Parentheses override operator precedence so that addition happens first. Expect the following + // tree: + // * + // + 2 + // 1 3 + val rootExpression = result.getExpectedSuccessfulExpression() + val rootMulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val rootLeftAddOp = rootMulOp.leftOperand.getExpectedBinaryOperationWithOperator(ADD) + val rootRightConstant = rootMulOp.rightOperand.getExpectedRationalConstant() + val innerOpLeftConstant = rootLeftAddOp.leftOperand.getExpectedRationalConstant() + val innerOpRightConstant = rootLeftAddOp.rightOperand.getExpectedRationalConstant() + assertThat(rootRightConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(1)) + assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(3)) + } + + @Test + fun testParse_withNestedParentheses_deeperParenthesesEvaluateFirst() { + val result = MathExpressionParser.parseExpression("1^(2*(1-x))", allowedVariables = listOf("x")) + + // Nested parentheses happen before earlier parentheses. Expect the following tree: + // ^ + // 1 * + // 2 - + // 1 x + val rootExpression = result.getExpectedSuccessfulExpression() + val rootExpOp = rootExpression.getExpectedBinaryOperationWithOperator(EXPONENTIATE) + val rootLeftConstant = rootExpOp.leftOperand.getExpectedRationalConstant() + val rootRightMulOp = rootExpOp.rightOperand.getExpectedBinaryOperationWithOperator(MULTIPLY) + val mulLeftConstant = rootRightMulOp.leftOperand.getExpectedRationalConstant() + val mulRightSubOp = rootRightMulOp.rightOperand.getExpectedBinaryOperationWithOperator(SUBTRACT) + val subLeftConstant = mulRightSubOp.leftOperand.getExpectedRationalConstant() + val subRightVar = mulRightSubOp.rightOperand.getExpectedVariable() + assertThat(rootLeftConstant).isEqualTo(createWholeNumberFraction(1)) + assertThat(mulLeftConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(subLeftConstant).isEqualTo(createWholeNumberFraction(1)) + assertThat(subRightVar).isEqualTo("x") + } + + @Test + fun testParse_combineMultiplicationSubtractionAndNegation_negationHasHighestPrecedence() { + // See: https://wcipeg.com/wiki/Shunting_yard_algorithm#Unary_operators. Unary negation is + // usually treated as higher precedence. + val result = MathExpressionParser.parseExpression("10/-1*-2", allowedVariables = listOf()) + + // Expected tree: + // * + // / - + // 10 - 2 + // 1 + val rootExpression = result.getExpectedSuccessfulExpression() + val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val divOp = mulOp.leftOperand.getExpectedBinaryOperationWithOperator(DIVIDE) + val rightMulNegateOp = mulOp.rightOperand.getExpectedUnaryOperationWithOperator(NEGATE) + val rightMulNegatedConstant = rightMulNegateOp.operand.getExpectedRationalConstant() + val leftDivConstant = divOp.leftOperand.getExpectedRationalConstant() + val rightDivNegateOp = divOp.rightOperand.getExpectedUnaryOperationWithOperator(NEGATE) + val rightDivNegatedConstant = rightDivNegateOp.operand.getExpectedRationalConstant() + assertThat(rightMulNegatedConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(leftDivConstant).isEqualTo(createWholeNumberFraction(10)) + assertThat(rightDivNegatedConstant).isEqualTo(createWholeNumberFraction(1)) + } + @Test + fun test_negationWithExponentiation_exponentiationHasHigherPrecedence() { + val result = MathExpressionParser.parseExpression("-2^3^4", allowedVariables = listOf()) + + // Expected tree (note negation happens last since it's lower precedence): + // - + // ^ + // 2 ^ + // 3 4 + val rootExpression = result.getExpectedSuccessfulExpression() + val negOp = rootExpression.getExpectedUnaryOperationWithOperator(NEGATE) + val secondExpOp = negOp.operand.getExpectedBinaryOperationWithOperator(EXPONENTIATE) + val secondExpLeftConstant = secondExpOp.leftOperand.getExpectedRationalConstant() + val firstExpOp = secondExpOp.rightOperand.getExpectedBinaryOperationWithOperator(EXPONENTIATE) + val firstExpLeftConstant = firstExpOp.leftOperand.getExpectedRationalConstant() + val firstExpRightConstant = firstExpOp.rightOperand.getExpectedRationalConstant() + assertThat(secondExpLeftConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(firstExpLeftConstant).isEqualTo(createWholeNumberFraction(3)) + assertThat(firstExpRightConstant).isEqualTo(createWholeNumberFraction(4)) + } + + @Test + fun testParse_complexExpression_followsPemdasWithAssociativity() { + val result = MathExpressionParser.parseExpression( + "1+(13+12)/15+3*2-7/4^2^3*x+-(2*(y-3.14))", + allowedVariables = listOf("x", "y") + ) + + // Look at past tests for associativity & precedence rules that are repeated here. Expect the + // following tree: + // + + // - - (negation) + // + * * + // + * / x 2 - + // 1 / 3 2 7 ^ y 3.14 + // + 15 4 ^ + // 13 12 2 3 + // Note that the enumeration below isn't done in evaluation order. The variables below are also + // preferring a leftward-leaning breadth-first enumeration. + val rootExpression = result.getExpectedSuccessfulExpression() + // Parse the root level: addition. + val lvl1Elem1Op = rootExpression.getExpectedBinaryOperationWithOperator(ADD) + // Next level: subtraction & negation. + val lvl2Elem1Op = lvl1Elem1Op.leftOperand.getExpectedBinaryOperationWithOperator(SUBTRACT) + val lvl2Elem2Op = lvl1Elem1Op.rightOperand.getExpectedUnaryOperationWithOperator(NEGATE) + // Next level: addition, multiplication, and multiplication. + val lvl3Elem1Op = lvl2Elem1Op.leftOperand.getExpectedBinaryOperationWithOperator(ADD) + val lvl3Elem2Op = lvl2Elem1Op.rightOperand.getExpectedBinaryOperationWithOperator(MULTIPLY) + val lvl3Elem3Op = lvl2Elem2Op.operand.getExpectedBinaryOperationWithOperator(MULTIPLY) + // Next level: addition, multiplication, division, variable x, constant 2, subtraction. + val lvl4Elem1Op = lvl3Elem1Op.leftOperand.getExpectedBinaryOperationWithOperator(ADD) + val lvl4Elem2Op = lvl3Elem1Op.rightOperand.getExpectedBinaryOperationWithOperator(MULTIPLY) + val lvl4Elem3Op = lvl3Elem2Op.leftOperand.getExpectedBinaryOperationWithOperator(DIVIDE) + val lvl4Elem4Var = lvl3Elem2Op.rightOperand.getExpectedVariable() + val lvl4Elem5Const = lvl3Elem3Op.leftOperand.getExpectedRationalConstant() + val lvl4Elem6Op = lvl3Elem3Op.rightOperand.getExpectedBinaryOperationWithOperator(SUBTRACT) + // Next level: 1, division, 3, 2, 7, exponentiation, y, 3.14. + val lvl5Elem1Const = lvl4Elem1Op.leftOperand.getExpectedRationalConstant() + val lvl5Elem2Op = lvl4Elem1Op.rightOperand.getExpectedBinaryOperationWithOperator(DIVIDE) + val lvl5Elem3Const = lvl4Elem2Op.leftOperand.getExpectedRationalConstant() + val lvl5Elem4Const = lvl4Elem2Op.rightOperand.getExpectedRationalConstant() + val lvl5Elem5Const = lvl4Elem3Op.leftOperand.getExpectedRationalConstant() + val lvl5Elem6Op = lvl4Elem3Op.rightOperand.getExpectedBinaryOperationWithOperator(EXPONENTIATE) + val lvl5Elem7Var = lvl4Elem6Op.leftOperand.getExpectedVariable() + val lvl5Elem8Const = lvl4Elem6Op.rightOperand.getExpectedIrrationalConstant() + // Next level: addition, 15, 4, exponentiation + val lvl6Elem1Op = lvl5Elem2Op.leftOperand.getExpectedBinaryOperationWithOperator(ADD) + val lvl6Elem2Const = lvl5Elem2Op.rightOperand.getExpectedRationalConstant() + val lvl6Elem3Const = lvl5Elem6Op.leftOperand.getExpectedRationalConstant() + val lvl6Elem4Op = lvl5Elem6Op.rightOperand.getExpectedBinaryOperationWithOperator(EXPONENTIATE) + // Final level: 13, 12, 2, 3 + val lvl7Elem1Const = lvl6Elem1Op.leftOperand.getExpectedRationalConstant() + val lvl7Elem2Const = lvl6Elem1Op.rightOperand.getExpectedRationalConstant() + val lvl7Elem3Const = lvl6Elem4Op.leftOperand.getExpectedRationalConstant() + val lvl7Elem4Const = lvl6Elem4Op.rightOperand.getExpectedRationalConstant() + + // Now, verify the constants and variables (cross reference with the level comments above & the + // tree to verify). + assertThat(lvl4Elem4Var).isEqualTo("x") + assertThat(lvl4Elem5Const).isEqualTo(createWholeNumberFraction(2)) + assertThat(lvl5Elem1Const).isEqualTo(createWholeNumberFraction(1)) + assertThat(lvl5Elem3Const).isEqualTo(createWholeNumberFraction(3)) + assertThat(lvl5Elem4Const).isEqualTo(createWholeNumberFraction(2)) + assertThat(lvl5Elem5Const).isEqualTo(createWholeNumberFraction(7)) + assertThat(lvl5Elem7Var).isEqualTo("y") + assertThat(lvl5Elem8Const).isWithin(1e-5).of(3.14) + assertThat(lvl6Elem2Const).isEqualTo(createWholeNumberFraction(15)) + assertThat(lvl6Elem3Const).isEqualTo(createWholeNumberFraction(4)) + assertThat(lvl7Elem1Const).isEqualTo(createWholeNumberFraction(13)) + assertThat(lvl7Elem2Const).isEqualTo(createWholeNumberFraction(12)) + assertThat(lvl7Elem3Const).isEqualTo(createWholeNumberFraction(2)) + assertThat(lvl7Elem4Const).isEqualTo(createWholeNumberFraction(3)) + } + + @Test + fun testParse_twoShortVariables_withoutOperator_impliesMultiplication() { + val result = MathExpressionParser.parseExpression("xy", allowedVariables = listOf("x", "y")) + + // Having two variables right next to each other implies multiplication. + val rootExpression = result.getExpectedSuccessfulExpression() + val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val leftVar = mulOp.leftOperand.getExpectedVariable() + val rightVar = mulOp.rightOperand.getExpectedVariable() + assertThat(leftVar).isEqualTo("x") + assertThat(rightVar).isEqualTo("y") + } + + @Test + fun testParse_constantWithShortVariable_withoutOperator_impliesMultiplication() { + val result = MathExpressionParser.parseExpression("2x", allowedVariables = listOf("x")) + + // Having a constant and a variable right next to each other implies multiplication. + val rootExpression = result.getExpectedSuccessfulExpression() + val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val leftConstant = mulOp.leftOperand.getExpectedRationalConstant() + val rightVar = mulOp.rightOperand.getExpectedVariable() + assertThat(leftConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(rightVar).isEqualTo("x") + } + + @Test + fun testParse_constantWithLongVariable_withoutOperator_impliesMultiplication() { + val result = MathExpressionParser.parseExpression( + "2lambda", allowedVariables = listOf("lambda") + ) + + // Having a constant and a variable right next to each other implies multiplication. + val rootExpression = result.getExpectedSuccessfulExpression() + val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val leftConstant = mulOp.leftOperand.getExpectedRationalConstant() + val rightVar = mulOp.rightOperand.getExpectedVariable() + assertThat(leftConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(rightVar).isEqualTo("lambda") + } + + @Test + fun testParse_constantWithMultipleVariables_ambiguous_withoutOperator_impliesLongVarMult() { + val result = MathExpressionParser.parseExpression( + "2xyz", allowedVariables = listOf("x", "y", "z", "xyz") + ) + + // The implied multiplication here is always on long variable since it's otherwise ambiguous. + val rootExpression = result.getExpectedSuccessfulExpression() + val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val leftConstant = mulOp.leftOperand.getExpectedRationalConstant() + val rightVar = mulOp.rightOperand.getExpectedVariable() + assertThat(leftConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(rightVar).isEqualTo("xyz") + } + + @Test + fun testParse_shortAndLongVariable_withoutOperator_failsToParse() { + val result = MathExpressionParser.parseExpression( + "wxyz", allowedVariables = listOf("w", "xyz") + ) + + // There can't be implied multiplication here since 'wxyz' looks like 1 variable. Note that if + // 'x', 'y', 'z' are separate allowed variables then the above *will* succeed in parsing an + // expression equivalent to w*x*y*z. + val failureReason = result.getExpectedFailedExpression() + assertThat(failureReason).contains("Encountered invalid identifier: wxyz") + } + + @Test + fun testParse_variableNextToConstant_withoutOperator_impliesMultiplication() { + val result = MathExpressionParser.parseExpression("x2", allowedVariables = listOf("x")) + + // Having a constant and a variable right next to each other implies multiplication. + val rootExpression = result.getExpectedSuccessfulExpression() + val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val leftVar = mulOp.leftOperand.getExpectedVariable() + val rightConstant = mulOp.rightOperand.getExpectedRationalConstant() + assertThat(leftVar).isEqualTo("x") + assertThat(rightConstant).isEqualTo(createWholeNumberFraction(2)) + } + + @Test + fun testParse_polynomialWithoutOperators_createsCorrectExpressionTree() { + val result = MathExpressionParser.parseExpression("2x^-2", allowedVariables = listOf("x")) + + // Basic polynomial expressions should parse as expected. For the above expression, expect the + // tree: + // * + // 2 ^ + // x - + // 2 + // Having a constant and a variable right next to each other implies multiplication. + val rootExpression = result.getExpectedSuccessfulExpression() + val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val leftMulConstant = mulOp.leftOperand.getExpectedRationalConstant() + val rightExpOp = mulOp.rightOperand.getExpectedBinaryOperationWithOperator(EXPONENTIATE) + val leftExpVar = rightExpOp.leftOperand.getExpectedVariable() + val rightExpNegOp = rightExpOp.rightOperand.getExpectedUnaryOperationWithOperator(NEGATE) + val negOpConst = rightExpNegOp.operand.getExpectedRationalConstant() + assertThat(leftMulConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(leftExpVar).isEqualTo("x") + assertThat(negOpConst).isEqualTo(createWholeNumberFraction(2)) + } + + @Test + fun testParse_multipleShortVariables_withoutOperators_impliesMultipleMultiplications() { + val result = + MathExpressionParser.parseExpression("xyz", allowedVariables = listOf("x", "y", "z")) + + // Having consecutive variables also implies multiplication. In this case, the expression uses + // left associativity, so expect the following tree: + // * + // * z + // x y + val rootExpression = result.getExpectedSuccessfulExpression() + val secondMulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val firstMulOp = secondMulOp.leftOperand.getExpectedBinaryOperationWithOperator(MULTIPLY) + val secondMulRightVar = secondMulOp.rightOperand.getExpectedVariable() + val firstMulLeftVar = firstMulOp.leftOperand.getExpectedVariable() + val firstMulRightVar = firstMulOp.rightOperand.getExpectedVariable() + assertThat(secondMulRightVar).isEqualTo("z") + assertThat(firstMulLeftVar).isEqualTo("x") + assertThat(firstMulRightVar).isEqualTo("y") + } + + @Test + fun testParse_shortVariables_withAmbiguousLongVariable_noOperators_impliesSingleVariable() { + val result = MathExpressionParser.parseExpression( + "xyz", allowedVariables = listOf("x", "y", "z", "xyz") + ) + + // 'xyz' is ambiguous in this case, but a single variable should be preferred since it's an + // exact match. + val rootExpression = result.getExpectedSuccessfulExpression() + val rootVar = rootExpression.getExpectedVariable() + assertThat(rootVar).isEqualTo("xyz") + } + + @Test + fun testParse_shortVariables_withAmbiguousLongVariable_withOperator_hasMultipleVariables() { + val result = MathExpressionParser.parseExpression( + "x*yz", allowedVariables = listOf("x", "y", "z", "xyz") + ) + + // Unlike the above test, the single operator is sufficient to disambiguate the the x, y, z vs. + // xyz variable dilemma, so expect the following tree: + // * + // * z + // x y + val rootExpression = result.getExpectedSuccessfulExpression() + val secondMulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val firstMulOp = secondMulOp.leftOperand.getExpectedBinaryOperationWithOperator(MULTIPLY) + val secondMulRightVar = secondMulOp.rightOperand.getExpectedVariable() + val firstMulLeftVar = firstMulOp.leftOperand.getExpectedVariable() + val firstMulRightVar = firstMulOp.rightOperand.getExpectedVariable() + assertThat(secondMulRightVar).isEqualTo("z") + assertThat(firstMulLeftVar).isEqualTo("x") + assertThat(firstMulRightVar).isEqualTo("y") + } + + @Test + fun testParse_polynomialMultiplication_withoutOperator_impliesMultiplication() { + val result = MathExpressionParser.parseExpression("(x+1)(x+2)", allowedVariables = listOf("x")) + + // Having two polynomials right next to each other implies multiplication. Expect the tree: + // * + // + + + // x 1 x 2 + val rootExpression = result.getExpectedSuccessfulExpression() + val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val leftAddOp = mulOp.leftOperand.getExpectedBinaryOperationWithOperator(ADD) + val rightAddOp = mulOp.rightOperand.getExpectedBinaryOperationWithOperator(ADD) + val leftAddLeftVar = leftAddOp.leftOperand.getExpectedVariable() + val leftAddRightVar = leftAddOp.rightOperand.getExpectedRationalConstant() + val rightAddLeftVar = rightAddOp.leftOperand.getExpectedVariable() + val rightAddRightVar = rightAddOp.rightOperand.getExpectedRationalConstant() + assertThat(leftAddLeftVar).isEqualTo("x") + assertThat(leftAddRightVar).isEqualTo(createWholeNumberFraction(1)) + assertThat(rightAddLeftVar).isEqualTo("x") + assertThat(rightAddRightVar).isEqualTo(createWholeNumberFraction(2)) + } + + @Test + fun testParse_constantAndPolynomialMultiplication_withoutOperator_impliesMultiplication() { + val result = MathExpressionParser.parseExpression("2(x+1)", allowedVariables = listOf("x")) + + // Having a constant and a polynomial right next to each other implies multiplication. Expect: + // * + // 2 + + // x 1 + val rootExpression = result.getExpectedSuccessfulExpression() + val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val leftConstant = mulOp.leftOperand.getExpectedRationalConstant() + val rightAddOp = mulOp.rightOperand.getExpectedBinaryOperationWithOperator(ADD) + val rightAddLeftVar = rightAddOp.leftOperand.getExpectedVariable() + val rightAddRightConstant = rightAddOp.rightOperand.getExpectedRationalConstant() + assertThat(leftConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(rightAddLeftVar).isEqualTo("x") + assertThat(rightAddRightConstant).isEqualTo(createWholeNumberFraction(1)) + } + + @Test + fun testParse_polynomialAndConstantMultiplication_withoutOperator_impliesMultiplication() { + val result = MathExpressionParser.parseExpression("(x+1)2", allowedVariables = listOf("x")) + + // Having a constant and a polynomial right next to each other implies multiplication. Expect: + // * + // + 2 + // x 1 + val rootExpression = result.getExpectedSuccessfulExpression() + val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val leftAddOp = mulOp.leftOperand.getExpectedBinaryOperationWithOperator(ADD) + val rightConstant = mulOp.rightOperand.getExpectedRationalConstant() + val leftAddLeftVar = leftAddOp.leftOperand.getExpectedVariable() + val leftAddRightConstant = leftAddOp.rightOperand.getExpectedRationalConstant() + assertThat(rightConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(leftAddLeftVar).isEqualTo("x") + assertThat(leftAddRightConstant).isEqualTo(createWholeNumberFraction(1)) + } + + @Test + fun testParse_variableAndPolynomialMultiplication_withoutOperator_impliesMultiplication() { + val result = MathExpressionParser.parseExpression("x(x+1)", allowedVariables = listOf("x")) + + // Having a constant and a polynomial right next to each other implies multiplication. Expect: + // * + // x + + // x 1 + val rootExpression = result.getExpectedSuccessfulExpression() + val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val leftVar = mulOp.leftOperand.getExpectedVariable() + val rightAddOp = mulOp.rightOperand.getExpectedBinaryOperationWithOperator(ADD) + val rightAddLeftVar = rightAddOp.leftOperand.getExpectedVariable() + val rightAddRightConstant = rightAddOp.rightOperand.getExpectedRationalConstant() + assertThat(leftVar).isEqualTo("x") + assertThat(rightAddLeftVar).isEqualTo("x") + assertThat(rightAddRightConstant).isEqualTo(createWholeNumberFraction(1)) + } + + @Test + fun testParse_polynomialAndVariableMultiplication_withoutOperator_impliesMultiplication() { + val result = MathExpressionParser.parseExpression("(x+1)x", allowedVariables = listOf("x")) + + // Having a constant and a polynomial right next to each other implies multiplication. Expect: + // * + // + 2 + // x 1 + val rootExpression = result.getExpectedSuccessfulExpression() + val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val leftAddOp = mulOp.leftOperand.getExpectedBinaryOperationWithOperator(ADD) + val rightVar = mulOp.rightOperand.getExpectedVariable() + val leftAddLeftVar = leftAddOp.leftOperand.getExpectedVariable() + val leftAddRightConstant = leftAddOp.rightOperand.getExpectedRationalConstant() + assertThat(rightVar).isEqualTo("x") + assertThat(leftAddLeftVar).isEqualTo("x") + assertThat(leftAddRightConstant).isEqualTo(createWholeNumberFraction(1)) + } + + @Test + fun testParse_emptyLiteral_failsToResolveTree() { + val result = MathExpressionParser.parseExpression("", allowedVariables = listOf()) + + val failureReason = result.getExpectedFailedExpression() + assertThat(failureReason).contains("Failed to resolve expression tree") + } + + @Test + fun testParse_twoConsecutiveConstants_failsToResolveTree() { + val result = MathExpressionParser.parseExpression("2 3", allowedVariables = listOf()) + + val failureReason = result.getExpectedFailedExpression() + assertThat(failureReason).contains("Failed to resolve expression tree") + } + + @Test + fun testParse_twoConsecutiveConstants_withParentheses_impliesMultiplication() { + val result = MathExpressionParser.parseExpression("(2) (3)", allowedVariables = listOf()) + + // Parentheses fully change the meaning since it now looks like polynomial multiplication. + val rootExpression = result.getExpectedSuccessfulExpression() + val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) + val leftConstant = mulOp.leftOperand.getExpectedRationalConstant() + val rightConstant = mulOp.rightOperand.getExpectedRationalConstant() + assertThat(leftConstant).isEqualTo(createWholeNumberFraction(2)) + assertThat(rightConstant).isEqualTo(createWholeNumberFraction(3)) + } + + @Test + fun testParse_mismatchedOpenParenthesis_failsWithUnresolvedParenthesis() { + val result = MathExpressionParser.parseExpression("(", allowedVariables = listOf()) + + val failureReason = result.getExpectedFailedExpression() + assertThat(failureReason).contains("unexpected open parenthesis") + } + + @Test + fun testParse_extraOpenParenthesis_failsWithUnresolvedParenthesis() { + val result = MathExpressionParser.parseExpression("((2)", allowedVariables = listOf()) + + val failureReason = result.getExpectedFailedExpression() + assertThat(failureReason).contains("unexpected open parenthesis") + } + + @Test + fun testParse_mismatchedCloseParenthesis_failsWithUnresolvedParenthesis() { + val result = MathExpressionParser.parseExpression(")", allowedVariables = listOf()) + + val failureReason = result.getExpectedFailedExpression() + assertThat(failureReason).contains("unexpected close parenthesis") + } + + @Test + fun testParse_extraCloseParenthesis_failsWithUnresolvedParenthesis() { + val result = MathExpressionParser.parseExpression("(2))", allowedVariables = listOf()) + + val failureReason = result.getExpectedFailedExpression() + assertThat(failureReason).contains("unexpected close parenthesis") + } + + @Test + fun testParse_mismatchedCloseParenthesis_failsToResolveTree() { + val result = MathExpressionParser.parseExpression("()", allowedVariables = listOf()) + + val failureReason = result.getExpectedFailedExpression() + assertThat(failureReason).contains("Failed to resolve expression tree") + } + + @Test + fun testParse_twoConsecutiveBinaryOperators_failsWithMissingBinaryOperand() { + val result = MathExpressionParser.parseExpression("2**3", allowedVariables = listOf()) + + val failureReason = result.getExpectedFailedExpression() + assertThat(failureReason).contains("Encountered binary operator with missing operand(s)") + } + + @Test + fun testParse_binaryOperator_missingRightOperand_failsWithMissingBinaryOperand() { + val result = MathExpressionParser.parseExpression("2*", allowedVariables = listOf()) + + val failureReason = result.getExpectedFailedExpression() + assertThat(failureReason).contains("Encountered binary operator with missing operand(s)") + } + + @Test + fun testParse_binaryOperator_missingLeftOperand_failsWithMissingBinaryOperand() { + val result = MathExpressionParser.parseExpression("*2", allowedVariables = listOf()) + + val failureReason = result.getExpectedFailedExpression() + assertThat(failureReason).contains("Encountered binary operator with missing operand(s)") + } + + @Test + fun testParse_binaryOperator_missingBothOperands_failsWithMissingBinaryOperand() { + val result = MathExpressionParser.parseExpression("*", allowedVariables = listOf()) + + val failureReason = result.getExpectedFailedExpression() + assertThat(failureReason).contains("Encountered binary operator with missing operand(s)") + } + + @Test + fun testParse_negation_missingOperand_failsWithMissingUnaryOperand() { + val result = MathExpressionParser.parseExpression("-", allowedVariables = listOf()) + + val failureReason = result.getExpectedFailedExpression() + assertThat(failureReason).contains("Encountered unary operator without operand") + } + + @Test + fun testParse_unknownUnarySymbol_failsWithUnexpectedSymbol() { + val result = MathExpressionParser.parseExpression("3!", allowedVariables = listOf()) + + val failureReason = result.getExpectedFailedExpression() + assertThat(failureReason).contains("unexpected symbol") + } + + @Test + fun testParse_unknownBinarySymbol_failsWithUnexpectedSymbol() { + val result = MathExpressionParser.parseExpression("2%3", allowedVariables = listOf()) + + val failureReason = result.getExpectedFailedExpression() + assertThat(failureReason).contains("unexpected symbol") + } + + @Test + fun testParse_withInvalidAllowedVariable_throwsException() { + val exception = assertThrows(IllegalArgumentException::class) { + MathExpressionParser.parseExpression("", allowedVariables = listOf("invalid!")) + } + + assertThat(exception).hasMessageThat().contains("contains non-letters") + } + + @Test + fun testParse_withEmptyAllowedVariable_throwsException() { + val exception = assertThrows(IllegalArgumentException::class) { + MathExpressionParser.parseExpression("", allowedVariables = listOf("")) + } + + assertThat(exception).hasMessageThat().contains("empty identifier") + } + + @Test + fun testParse_expressionWithUndefinedVariable_failsWithUnexpectedIdentifier() { + val result = MathExpressionParser.parseExpression("x", allowedVariables = listOf()) + + val failureReason = result.getExpectedFailedExpression() + assertThat(failureReason).contains("invalid identifier: x") + } + + @Test + fun testParse_expressionWithInvalidCharacter_failsWithUnexpectedSymbol() { + val result = MathExpressionParser.parseExpression("∫", allowedVariables = listOf()) + + val failureReason = result.getExpectedFailedExpression() + assertThat(failureReason).contains("unexpected symbol") + } + + private fun ParseResult.getExpectedSuccessfulExpression(): MathExpression { + assertThat(this).isInstanceOf(Success::class.java) + return (this as Success).mathExpression + } + + private fun ParseResult.getExpectedFailedExpression(): String { + assertThat(this).isInstanceOf(Failure::class.java) + return (this as Failure).failureReason + } + + private fun MathExpression.getExpectedConstant(): Real { + return getExpectedType(MathExpression::getConstant, CONSTANT) + } + + private fun MathExpression.getExpectedRationalConstant(): Fraction { + val constant = getExpectedType(MathExpression::getConstant, CONSTANT) + assertThat(constant.realTypeCase).isEqualTo(RATIONAL) + return constant.rational + } + + private fun MathExpression.getExpectedIrrationalConstant(): Double { + val constant = getExpectedType(MathExpression::getConstant, CONSTANT) + assertThat(constant.realTypeCase).isEqualTo(IRRATIONAL) + return constant.irrational + } + + private fun MathExpression.getExpectedVariable(): String { + return getExpectedType(MathExpression::getVariable, VARIABLE) + } + + private fun MathExpression.getExpectedUnaryOperation(): MathUnaryOperation { + return getExpectedType(MathExpression::getUnaryOperation, UNARY_OPERATION) + } + + private fun MathExpression.getExpectedUnaryOperationWithOperator( + operator: MathUnaryOperation.Operator + ): MathUnaryOperation { + val expectedOp = getExpectedType(MathExpression::getUnaryOperation, UNARY_OPERATION) + assertThat(expectedOp.operator).isEqualTo(operator) + return expectedOp + } + + private fun MathExpression.getExpectedBinaryOperation(): MathBinaryOperation { + return getExpectedType(MathExpression::getBinaryOperation, BINARY_OPERATION) + } + + private fun MathExpression.getExpectedBinaryOperationWithOperator( + operator: MathBinaryOperation.Operator + ): MathBinaryOperation { + val expectedOp = getExpectedType(MathExpression::getBinaryOperation, BINARY_OPERATION) + assertThat(expectedOp.operator).isEqualTo(operator) + return expectedOp + } + + private fun MathExpression.getExpectedType( + typeRetriever: MathExpression.() -> T, + expectedType: ExpressionTypeCase + ): T { + assertThat(expressionTypeCase).isEqualTo(expectedType) + return typeRetriever() } private fun createWholeNumberFraction(value: Int): Fraction { - return Fraction.newBuilder().setWholeNumber(value).build() + return Fraction.newBuilder().setWholeNumber(value).setDenominator(1).build() + } + + // TODO(#89): Move to a common test library. + private fun assertThrows(type: KClass, operation: () -> Unit): T { + try { + operation() + fail("Expected to encounter exception of $type") + } catch (t: Throwable) { + if (type.isInstance(t)) { + return type.cast(t) + } + // Unexpected exception; throw it. + throw t + } } } diff --git a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt index 3320bd88250..62ac32ec2be 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt @@ -27,14 +27,14 @@ class MathTokenizerTest { @Test fun testTokenize_emptyString_producesNoTokens() { - val tokens = MathTokenizer.tokenize("").toList() + val tokens = MathTokenizer.tokenize("", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).isEmpty() } @Test fun testTokenize_wholeNumber_oneDigit_producesWholeNumberToken() { - val tokens = MathTokenizer.tokenize("1").toList() + val tokens = MathTokenizer.tokenize("1", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(WholeNumber::class.java) @@ -43,7 +43,7 @@ class MathTokenizerTest { @Test fun testTokenize_wholeNumber_multipleDigits_producesWholeNumberToken() { - val tokens = MathTokenizer.tokenize("913").toList() + val tokens = MathTokenizer.tokenize("913", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(WholeNumber::class.java) @@ -52,7 +52,7 @@ class MathTokenizerTest { @Test fun testTokenize_wholeNumber_zeroLeadingNumber_producesCorrectBase10WholeNumberToken() { - val tokens = MathTokenizer.tokenize("0913").toList() + val tokens = MathTokenizer.tokenize("0913", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(WholeNumber::class.java) @@ -61,34 +61,34 @@ class MathTokenizerTest { @Test fun testTokenize_decimalNumber_decimalLessThanOne_noZero_producesCorrectDecimalNumberToken() { - val tokens = MathTokenizer.tokenize(".14").toList() + val tokens = MathTokenizer.tokenize(".14", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(DecimalNumber::class.java) - assertThat((tokens.first() as DecimalNumber).value).isWithin(1e-3).of(0.14) + assertThat((tokens.first() as DecimalNumber).value).isWithin(1e-5).of(0.14) } @Test fun testTokenize_decimalNumber_decimalLessThanOne_withZero_producesCorrectDecimalNumberToken() { - val tokens = MathTokenizer.tokenize("0.14").toList() + val tokens = MathTokenizer.tokenize("0.14", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(DecimalNumber::class.java) - assertThat((tokens.first() as DecimalNumber).value).isWithin(1e-3).of(0.14) + assertThat((tokens.first() as DecimalNumber).value).isWithin(1e-5).of(0.14) } @Test fun testTokenize_decimalNumber_decimalGreaterThanOne_producesCorrectDecimalNumberToken() { - val tokens = MathTokenizer.tokenize("3.14").toList() + val tokens = MathTokenizer.tokenize("3.14", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(DecimalNumber::class.java) - assertThat((tokens.first() as DecimalNumber).value).isWithin(1e-3).of(3.14) + assertThat((tokens.first() as DecimalNumber).value).isWithin(1e-5).of(3.14) } @Test fun testTokenize_decimalNumber_decimalPointOnly_producesInvalidToken() { - val tokens = MathTokenizer.tokenize(".").toList() + val tokens = MathTokenizer.tokenize(".", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(InvalidToken::class.java) @@ -97,7 +97,7 @@ class MathTokenizerTest { @Test fun testTokenize_openParenthesis_producesOpenParenthesisToken() { - val tokens = MathTokenizer.tokenize("(").toList() + val tokens = MathTokenizer.tokenize("(", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(OpenParenthesis::class.java) @@ -105,7 +105,7 @@ class MathTokenizerTest { @Test fun testTokenize_closeParenthesis_producesCloseParenthesisToken() { - val tokens = MathTokenizer.tokenize(")").toList() + val tokens = MathTokenizer.tokenize(")", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(CloseParenthesis::class.java) @@ -113,7 +113,7 @@ class MathTokenizerTest { @Test fun testTokenize_plusSign_producesOperatorToken() { - val tokens = MathTokenizer.tokenize("+").toList() + val tokens = MathTokenizer.tokenize("+", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(Operator::class.java) @@ -122,7 +122,7 @@ class MathTokenizerTest { @Test fun testTokenize_minusSign_producesOperatorToken() { - val tokens = MathTokenizer.tokenize("-").toList() + val tokens = MathTokenizer.tokenize("-", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(Operator::class.java) @@ -131,7 +131,7 @@ class MathTokenizerTest { @Test fun testTokenize_asterisk_producesOperatorToken() { - val tokens = MathTokenizer.tokenize("*").toList() + val tokens = MathTokenizer.tokenize("*", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(Operator::class.java) @@ -140,7 +140,7 @@ class MathTokenizerTest { @Test fun testTokenize_formalMultiplicationSign_producesAsteriskOperatorToken() { - val tokens = MathTokenizer.tokenize("×").toList() + val tokens = MathTokenizer.tokenize("×", ALLOWED_XYZ_VARIABLES).toList() // The formal math multiplication symbol is translated to the conventional one for simplicity. assertThat(tokens).hasSize(1) @@ -150,7 +150,7 @@ class MathTokenizerTest { @Test fun testTokenize_forwardSlash_producesOperatorToken() { - val tokens = MathTokenizer.tokenize("/").toList() + val tokens = MathTokenizer.tokenize("/", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(Operator::class.java) @@ -159,7 +159,7 @@ class MathTokenizerTest { @Test fun testTokenize_formalDivisionSign_producesForwardSlashOperatorToken() { - val tokens = MathTokenizer.tokenize("÷").toList() + val tokens = MathTokenizer.tokenize("÷", ALLOWED_XYZ_VARIABLES).toList() // The formal math division symbol is translated to the conventional one for simplicity. assertThat(tokens).hasSize(1) @@ -169,7 +169,7 @@ class MathTokenizerTest { @Test fun testTokenize_caret_producesOperatorToken() { - val tokens = MathTokenizer.tokenize("^").toList() + val tokens = MathTokenizer.tokenize("^", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(Operator::class.java) @@ -178,7 +178,7 @@ class MathTokenizerTest { @Test fun testTokenize_exclamation_producesInvalidToken() { - val tokens = MathTokenizer.tokenize("!").toList() + val tokens = MathTokenizer.tokenize("!", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(InvalidToken::class.java) @@ -186,8 +186,8 @@ class MathTokenizerTest { } @Test - fun testTokenize_defaultIdentifier_producesIdentifierToken() { - val tokens = MathTokenizer.tokenize("x").toList() + fun testTokenize_validIdentifier_withAllowedIds_producesIdentifierToken() { + val tokens = MathTokenizer.tokenize("x", allowedIdentifiers = listOf("x")).toList() assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(Identifier::class.java) @@ -195,7 +195,7 @@ class MathTokenizerTest { } @Test - fun testTokenize_defaultIdentifier_withNoIdentifiersProvided_producesInvalidIdentifierToken() { + fun testTokenize_validIdentifier_withNoIdentifiersProvided_producesInvalidIdentifierToken() { val tokens = MathTokenizer.tokenize("x", allowedIdentifiers = listOf()).toList() assertThat(tokens).hasSize(1) @@ -204,7 +204,7 @@ class MathTokenizerTest { } @Test - fun testTokenize_defaultIdentifier_withInvalidAllowedIdentifiers_throwsException() { + fun testTokenize_withInvalidAllowedIdentifiers_throwsException() { val exception = assertThrows(IllegalArgumentException::class) { MathTokenizer.tokenize("x", allowedIdentifiers = listOf("valid", "invalid!")).toList() } @@ -213,16 +213,16 @@ class MathTokenizerTest { } @Test - fun testTokenize_nonDefaultIdentifier_withDefaultIdentifiers_producesInvalidIdentifierToken() { - val tokens = MathTokenizer.tokenize("z").toList() + fun testTokenize_withEmptyAllowedIdentifier_throwsException() { + val exception = assertThrows(IllegalArgumentException::class) { + MathTokenizer.tokenize("x", allowedIdentifiers = listOf("valid", "")).toList() + } - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(InvalidIdentifier::class.java) - assertThat((tokens.first() as InvalidIdentifier).name).isEqualTo("z") + assertThat(exception).hasMessageThat().contains("Encountered empty identifier") } @Test - fun testTokenize_nonDefaultIdentifier_withAllowedIdentifiers_producesIdentifierToken() { + fun testTokenize_withAllowedIdentifiers_producesIdentifierToken() { val tokens = MathTokenizer.tokenize("z", allowedIdentifiers = listOf("z")).toList() assertThat(tokens).hasSize(1) @@ -231,7 +231,7 @@ class MathTokenizerTest { } @Test - fun testTokenize_nonDefaultIdentifierLowercase_withAllowedIdentifiersUpper_producesIdToken() { + fun testTokenize_expressionWithIdLowercase_withAllowedIdentifiersUpper_producesIdToken() { val tokens = MathTokenizer.tokenize("z", allowedIdentifiers = listOf("Z")).toList() assertThat(tokens).hasSize(1) @@ -240,7 +240,7 @@ class MathTokenizerTest { } @Test - fun testTokenize_nonDefaultIdentifierUppercase_withAllowedIdentifiersLower_producesIdToken() { + fun testTokenize_expressionWithIdUppercase_withAllowedIdentifiersLower_producesIdToken() { val tokens = MathTokenizer.tokenize("Z", allowedIdentifiers = listOf("z")).toList() assertThat(tokens).hasSize(1) @@ -249,7 +249,7 @@ class MathTokenizerTest { } @Test - fun testTokenize_nonDefaultIdentifierUppercase_withAllowedIdentifiersUpper_producesIdToken() { + fun testTokenize_expressionWithIdUppercase_withAllowedIdentifiersUpper_producesIdToken() { val tokens = MathTokenizer.tokenize("Z", allowedIdentifiers = listOf("Z")).toList() assertThat(tokens).hasSize(1) @@ -299,7 +299,7 @@ class MathTokenizerTest { @Test fun testTokenize_invalidMultiWordIdentifier_missingFromAllowedList_producesInvalidIdToken() { - val tokens = MathTokenizer.tokenize("xyz").toList() + val tokens = MathTokenizer.tokenize("xyz", allowedIdentifiers = listOf()).toList() // Note that even though 'x' and 'y' are valid single-letter variables, because 'z' is // encountered the whole set of letters is considered a single invalid variable. @@ -450,7 +450,7 @@ class MathTokenizerTest { @Test fun testTokenize_identifier_whitespaceBefore_isIgnored() { - val tokens = MathTokenizer.tokenize(" \r\t\n x").toList() + val tokens = MathTokenizer.tokenize(" \r\t\n x", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(Identifier::class.java) @@ -459,7 +459,7 @@ class MathTokenizerTest { @Test fun testTokenize_identifier_whitespaceAfter_isIgnored() { - val tokens = MathTokenizer.tokenize("x \r\t\n ").toList() + val tokens = MathTokenizer.tokenize("x \r\t\n ", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(Identifier::class.java) @@ -468,7 +468,7 @@ class MathTokenizerTest { @Test fun testTokenize_identifierAndOperator_whitespaceBetween_isIgnored() { - val tokens = MathTokenizer.tokenize("- \r\t\n x").toList() + val tokens = MathTokenizer.tokenize("- \r\t\n x", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(2) assertThat(tokens[0]).isInstanceOf(Operator::class.java) @@ -479,7 +479,7 @@ class MathTokenizerTest { @Test fun testTokenize_digits_withSpaces_producesMultipleWholeNumberTokens() { - val tokens = MathTokenizer.tokenize("1 23 4").toList() + val tokens = MathTokenizer.tokenize("1 23 4", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(3) assertThat(tokens[0]).isInstanceOf(WholeNumber::class.java) @@ -492,7 +492,8 @@ class MathTokenizerTest { @Test fun testTokenize_complexExpressionWithAllTokenTypes_tokenizesEverythingInOrder() { - val tokens = MathTokenizer.tokenize("133 + 3.14 * x / (11 - 15) ^ 2 ^ 3").toList() + val tokens = + MathTokenizer.tokenize("133 + 3.14 * x / (11 - 15) ^ 2 ^ 3", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(15) @@ -503,7 +504,7 @@ class MathTokenizerTest { assertThat((tokens[1] as Operator).operator).isEqualTo('+') assertThat(tokens[2]).isInstanceOf(DecimalNumber::class.java) - assertThat((tokens[2] as DecimalNumber).value).isWithin(1e-3).of(3.14) + assertThat((tokens[2] as DecimalNumber).value).isWithin(1e-5).of(3.14) assertThat(tokens[3]).isInstanceOf(Operator::class.java) assertThat((tokens[3] as Operator).operator).isEqualTo('*') @@ -540,6 +541,15 @@ class MathTokenizerTest { assertThat((tokens[14] as WholeNumber).value).isEqualTo(3) } + @Test + fun testTokenize_integralSymbol_producesInvalidToken() { + val tokens = MathTokenizer.tokenize("∫", ALLOWED_XYZ_VARIABLES).toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens.first()).isInstanceOf(InvalidToken::class.java) + assertThat((tokens.first() as InvalidToken).token).isEqualTo("∫") + } + // TODO(#89): Move to a common test library. private fun assertThrows(type: KClass, operation: () -> Unit): T { try { From ffacf5d6863abb684e088550e599b52b286921be Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 11 Dec 2020 21:10:21 -0800 Subject: [PATCH 054/289] Fix typo. --- .../java/org/oppia/android/util/math/MathExpressionParser.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index 5b26ce42ce9..473dbe97034 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -350,7 +350,7 @@ class MathExpressionParser { } /** - * Corresponds to relative associativity for other encounterd operations whose operators are + * Corresponds to relative associativity for other encountered operations whose operators are * at the same level of precedence. */ enum class Associativity { From 9530d12aa3cba11d1ab5d464109b89517178328f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 15 Dec 2020 21:10:18 -0800 Subject: [PATCH 055/289] Fix most tests broken in Bazel after syncing. --- app/BUILD.bazel | 6 +-- domain/BUILD.bazel | 16 +++++++- ...enominatorEqualToRuleClassifierProvider.kt | 3 +- ...artExactlyEqualToRuleClassifierProvider.kt | 3 +- ...ntegerPartEqualToRuleClassifierProvider.kt | 3 +- ...sNoFractionalPartRuleClassifierProvider.kt | 3 +- ...sNumeratorEqualToRuleClassifierProvider.kt | 3 +- ...AndInSimplestFormRuleClassifierProvider.kt | 3 +- ...putIsEquivalentToRuleClassifierProvider.kt | 3 +- ...tIsExactlyEqualToRuleClassifierProvider.kt | 3 +- ...nputIsGreaterThanRuleClassifierProvider.kt | 3 +- ...onInputIsLessThanRuleClassifierProvider.kt | 3 +- ...tainsAtLeastOneOfRuleClassifierProvider.kt | 3 +- ...ntainAtLeastOneOfRuleClassifierProvider.kt | 3 +- ...ectionInputEqualsRuleClassifierProvider.kt | 3 +- ...tIsProperSubsetOfRuleClassifierProvider.kt | 3 +- ...ithUnitsIsEqualToRuleClassifierProvider.kt | 3 +- ...itsIsEquivalentToRuleClassifierProvider.kt | 3 +- ...aterThanOrEqualToRuleClassifierProvider.kt | 3 +- ...nputIsGreaterThanRuleClassifierProvider.kt | 3 +- ...nclusivelyBetweenRuleClassifierProvider.kt | 3 +- ...LessThanOrEqualToRuleClassifierProvider.kt | 3 +- ...icInputIsLessThanRuleClassifierProvider.kt | 3 +- ...IsWithinToleranceRuleClassifierProvider.kt | 3 +- .../RatioInputEqualsRuleClassifierProvider.kt | 3 +- ...sNumberOfTermsEqualToClassifierProvider.kt | 3 +- ...InputIsEquivalentRuleClassifierProvider.kt | 3 +- ...seSensitiveEqualsRuleClassifierProvider.kt | 3 +- ...tInputFuzzyEqualsRuleClassifierProvider.kt | 3 +- ...xtInputStartsWithRuleClassifierProvider.kt | 3 +- .../loguploader/FakeLogUploader.kt | 35 +++++++++++++++++ .../domain/oppialogger/OppiaLoggerTest.kt | 15 ++++---- .../analytics/AnalyticsControllerTest.kt | 16 ++++---- .../LogUploadWorkManagerInitializerTest.kt | 38 +------------------ .../loguploader/LogUploadWorkerTest.kt | 6 ++- 35 files changed, 130 insertions(+), 86 deletions(-) create mode 100644 domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/FakeLogUploader.kt diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 294d200cad4..ad0269475b5 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -1048,9 +1048,9 @@ app_test( # TODO(#973): Fix app module tests for Robolectric. app_test( - name = "NavigationDrawerTestActivityTest", - srcs = [test_with_resources("src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerTestActivityTest.kt")], - test_class = "org.oppia.android.app.testing.NavigationDrawerTestActivityTest", + name = "NavigationDrawerActivityTest", + srcs = [test_with_resources("src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityTest.kt")], + test_class = "org.oppia.android.app.testing.NavigationDrawerActivityTest", deps = TEST_DEPS, ) diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index e5a76799fdc..e318f0873e8 100644 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -10,7 +10,10 @@ load("//domain:domain_test.bzl", "domain_test") kt_android_library( name = "domain", - srcs = glob(["src/main/java/org/oppia/android/domain/**/*.kt"]), + srcs = glob( + ["src/main/java/org/oppia/android/domain/**/*.kt"], + exclude=["src/main/java/org/oppia/android/domain/testing/**/*.kt"] + ), assets = glob(["src/main/assets/**"]), assets_dir = "src/main/assets/", custom_package = "org.oppia.android.domain", @@ -23,6 +26,16 @@ kt_android_library( ], ) +kt_android_library( + name = "testing", + testonly = True, + srcs = glob(["src/main/java/org/oppia/android/domain/testing/**/*.kt"]), + deps = [ + ":domain", + artifact("androidx.work:work-runtime-ktx:2.4.0"), + ], +) + # TODO(#2143): Move InteractionObjectTestBuilder to a testing package outside the test folder. kt_android_library( name = "interaction_object_test_builder", @@ -39,6 +52,7 @@ TEST_DEPS = [ ":dagger", ":domain", ":interaction_object_test_builder", + ":testing", "//data:persistent_cache_store", "//model", "//testing", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProvider.kt index 920d80da61f..0e64186bf88 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProvider.kt @@ -13,7 +13,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/FractionInput/directives/fraction-input-rules.service.ts#L55 */ -internal class FractionInputHasDenominatorEqualToRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class FractionInputHasDenominatorEqualToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.MultiTypeSingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider.kt index abd7a5e524d..0cf0436d126 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider.kt @@ -13,7 +13,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/FractionInput/directives/fraction-input-rules.service.ts#L61 */ -internal class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider +// TODO(#1580): Re-restrict access using Bazel visibilities +class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProvider.kt index d24052e7e67..936de86e002 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProvider.kt @@ -13,7 +13,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/FractionInput/directives/fraction-input-rules.service.ts#L48 */ -internal class FractionInputHasIntegerPartEqualToRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class FractionInputHasIntegerPartEqualToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.MultiTypeSingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProvider.kt index 53be10e46a0..58f9c867e79 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProvider.kt @@ -13,7 +13,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/FractionInput/directives/fraction-input-rules.service.ts#L58 */ -internal class FractionInputHasNoFractionalPartRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class FractionInputHasNoFractionalPartRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.NoInputInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProvider.kt index c4b57344f10..6ac6548b17b 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProvider.kt @@ -13,7 +13,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/FractionInput/directives/fraction-input-rules.service.ts#L52 */ -internal class FractionInputHasNumeratorEqualToRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class FractionInputHasNumeratorEqualToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.MultiTypeSingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt index bffab3f96d6..1a594f1e68b 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt @@ -16,7 +16,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/FractionInput/directives/fraction-input-rules.service.ts#L32 */ -internal class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider +// TODO(#1580): Re-restrict access using Bazel visibilities +class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt index 3f327c293f3..2fb33b8a49e 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt @@ -15,7 +15,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/FractionInput/directives/fraction-input-rules.service.ts#L29 */ -internal class FractionInputIsEquivalentToRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class FractionInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProvider.kt index f6bf11e37c6..6d598462f21 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProvider.kt @@ -13,7 +13,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/FractionInput/directives/fraction-input-rules.service.ts#L38 */ -internal class FractionInputIsExactlyEqualToRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class FractionInputIsExactlyEqualToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt index 7f6134b77c2..b97634272a6 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt @@ -14,7 +14,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/FractionInput/directives/fraction-input-rules.service.ts#L45 */ -internal class FractionInputIsGreaterThanRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class FractionInputIsGreaterThanRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt index 2af4756865e..7daa45360ee 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt @@ -14,7 +14,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/FractionInput/directives/fraction-input-rules.service.ts#L42 */ -internal class FractionInputIsLessThanRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class FractionInputIsLessThanRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider.kt index ccecf8cf291..970e1a039c2 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider.kt @@ -13,7 +13,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/ItemSelectionInput/directives/item-selection-input-rules.service.ts#L32 */ -internal class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider.kt index 0b2ce625d99..4e060c308fb 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider.kt @@ -13,7 +13,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/ItemSelectionInput/directives/item-selection-input-rules.service.ts#L41 */ -internal class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider +// TODO(#1580): Re-restrict access using Bazel visibilities +class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProvider.kt index 4323babbcbf..2a0be24f078 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProvider.kt @@ -13,7 +13,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/ItemSelectionInput/directives/item-selection-input-rules.service.ts#L24 */ -internal class ItemSelectionInputEqualsRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class ItemSelectionInputEqualsRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProvider.kt index 6e773b1ea7b..c89b1d07743 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProvider.kt @@ -13,7 +13,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/ItemSelectionInput/directives/item-selection-input-rules.service.ts#L50 */ -internal class ItemSelectionInputIsProperSubsetOfRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class ItemSelectionInputIsProperSubsetOfRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt index 2d041af1383..b9c3fc9647f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt @@ -15,7 +15,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/NumberWithUnits/directives/number-with-units-rules.service.ts#L34 */ -internal class NumberWithUnitsIsEqualToRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class NumberWithUnitsIsEqualToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt index e5d66332c9d..2572709908f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt @@ -15,7 +15,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/NumberWithUnits/directives/number-with-units-rules.service.ts#L48 */ -internal class NumberWithUnitsIsEquivalentToRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class NumberWithUnitsIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProvider.kt index 6416890a774..b6b2d3877af 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProvider.kt @@ -11,7 +11,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/NumericInput/directives/numeric-input-rules.service.ts#L33 */ -internal class NumericInputIsGreaterThanOrEqualToRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class NumericInputIsGreaterThanOrEqualToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProvider.kt index 981f9064ae5..d1980129de1 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProvider.kt @@ -11,7 +11,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/NumericInput/directives/numeric-input-rules.service.ts#L27 */ -internal class NumericInputIsGreaterThanRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class NumericInputIsGreaterThanRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProvider.kt index 3205c34c470..942f774aeee 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProvider.kt @@ -12,7 +12,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/NumericInput/directives/numeric-input-rules.service.ts#L36 */ -internal class NumericInputIsInclusivelyBetweenRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class NumericInputIsInclusivelyBetweenRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.DoubleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProvider.kt index 5685ae912be..122139a32c2 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProvider.kt @@ -11,7 +11,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/NumericInput/directives/numeric-input-rules.service.ts#L30 */ -internal class NumericInputIsLessThanOrEqualToRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class NumericInputIsLessThanOrEqualToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProvider.kt index 2d737b89b3a..dfef211d916 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProvider.kt @@ -11,7 +11,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/NumericInput/directives/numeric-input-rules.service.ts#L24 */ -internal class NumericInputIsLessThanRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class NumericInputIsLessThanRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProvider.kt index 3097fe02137..63c38aa8116 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProvider.kt @@ -12,7 +12,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/NumericInput/directives/numeric-input-rules.service.ts#L41 */ -internal class NumericInputIsWithinToleranceRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class NumericInputIsWithinToleranceRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.DoubleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProvider.kt index e27bb871863..cd25fb88cbf 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProvider.kt @@ -11,7 +11,8 @@ import javax.inject.Inject * Provider for a classifier that determines whether two object are equal per the ratio input * interaction. */ -internal class RatioInputEqualsRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class RatioInputEqualsRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProvider.kt index e7428765bdf..ef0697a3b68 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProvider.kt @@ -10,7 +10,8 @@ import javax.inject.Inject /** * Provider for a classifier that determines whether two object have an equal number of terms. */ -internal class RatioInputHasNumberOfTermsEqualToClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class RatioInputHasNumberOfTermsEqualToClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.MultiTypeSingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt index 2c6f24b7b52..3cdf2010e15 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt @@ -12,7 +12,8 @@ import javax.inject.Inject * Provider for a classifier that determines whether two object are equal by converting them into * their lowest form as per the ratio input interaction. */ -internal class RatioInputIsEquivalentRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class RatioInputIsEquivalentRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputCaseSensitiveEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputCaseSensitiveEqualsRuleClassifierProvider.kt index 61646b51790..ac9245f0ab3 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputCaseSensitiveEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputCaseSensitiveEqualsRuleClassifierProvider.kt @@ -13,7 +13,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/TextInput/directives/text-input-rules.service.ts#L59 */ -internal class TextInputCaseSensitiveEqualsRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class TextInputCaseSensitiveEqualsRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt index 7cef1505cdb..45a840cf78f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt @@ -12,7 +12,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/TextInput/directives/text-input-rules.service.ts#L29. */ -internal class TextInputFuzzyEqualsRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class TextInputFuzzyEqualsRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt index e1bf898eca2..5c7869994d9 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt @@ -13,7 +13,8 @@ import javax.inject.Inject * * https://github.com/oppia/oppia/blob/37285a/extensions/interactions/TextInput/directives/text-input-rules.service.ts#L64 */ -internal class TextInputStartsWithRuleClassifierProvider @Inject constructor( +// TODO(#1580): Re-restrict access using Bazel visibilities +class TextInputStartsWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory ) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { diff --git a/domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/FakeLogUploader.kt b/domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/FakeLogUploader.kt new file mode 100644 index 00000000000..e9c399663f9 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/FakeLogUploader.kt @@ -0,0 +1,35 @@ +package org.oppia.android.domain.testing.oppialogger.loguploader + +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager +import org.oppia.android.util.logging.LogUploader +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton + +/** A test specific fake for the log uploader. */ +@Singleton +class FakeLogUploader @Inject constructor(): LogUploader { + private val eventRequestIdList = mutableListOf() + private val exceptionRequestIdList = mutableListOf() + + override fun enqueueWorkRequestForEvents( + workManager: WorkManager, + workRequest: PeriodicWorkRequest + ) { + eventRequestIdList.add(workRequest.id) + } + + override fun enqueueWorkRequestForExceptions( + workManager: WorkManager, + workRequest: PeriodicWorkRequest + ) { + exceptionRequestIdList.add(workRequest.id) + } + + /** Returns the most recent work request id that's stored in the [eventRequestIdList]. */ + fun getMostRecentEventRequestId() = eventRequestIdList.last() + + /** Returns the most recent work request id that's stored in the [exceptionRequestIdList]. */ + fun getMostRecentExceptionRequestId() = exceptionRequestIdList.last() +} diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/OppiaLoggerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/OppiaLoggerTest.kt index 30de9feba66..8b1de99fd70 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/OppiaLoggerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/OppiaLoggerTest.kt @@ -13,13 +13,6 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.EventLog -import org.oppia.android.domain.oppialogger.analytics.TEST_EXPLORATION_ID -import org.oppia.android.domain.oppialogger.analytics.TEST_QUESTION_ID -import org.oppia.android.domain.oppialogger.analytics.TEST_SKILL_ID -import org.oppia.android.domain.oppialogger.analytics.TEST_SKILL_LIST_ID -import org.oppia.android.domain.oppialogger.analytics.TEST_STORY_ID -import org.oppia.android.domain.oppialogger.analytics.TEST_SUB_TOPIC_ID -import org.oppia.android.domain.oppialogger.analytics.TEST_TOPIC_ID import org.oppia.android.testing.TestDispatcherModule import org.oppia.android.testing.TestLogReportingModule import org.oppia.android.util.logging.EnableConsoleLog @@ -31,6 +24,14 @@ import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +private const val TEST_TOPIC_ID = "test_topicId" +private const val TEST_STORY_ID = "test_storyId" +private const val TEST_EXPLORATION_ID = "test_explorationId" +private const val TEST_QUESTION_ID = "test_questionId" +private const val TEST_SKILL_ID = "test_skillId" +private const val TEST_SKILL_LIST_ID = "test_skillListId" +private const val TEST_SUB_TOPIC_ID = 1 + @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt index 9d6bd5a0cd2..8afb1abf901 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt @@ -53,14 +53,14 @@ import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -const val TEST_TIMESTAMP = 1556094120000 -const val TEST_TOPIC_ID = "test_topicId" -const val TEST_STORY_ID = "test_storyId" -const val TEST_EXPLORATION_ID = "test_explorationId" -const val TEST_QUESTION_ID = "test_questionId" -const val TEST_SKILL_ID = "test_skillId" -const val TEST_SKILL_LIST_ID = "test_skillListId" -const val TEST_SUB_TOPIC_ID = 1 +private const val TEST_TIMESTAMP = 1556094120000 +private const val TEST_TOPIC_ID = "test_topicId" +private const val TEST_STORY_ID = "test_storyId" +private const val TEST_EXPLORATION_ID = "test_explorationId" +private const val TEST_QUESTION_ID = "test_questionId" +private const val TEST_SKILL_ID = "test_skillId" +private const val TEST_SKILL_LIST_ID = "test_skillListId" +private const val TEST_SUB_TOPIC_ID = 1 @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkManagerInitializerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkManagerInitializerTest.kt index 3056ca454e1..d0f6ba4338e 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkManagerInitializerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkManagerInitializerTest.kt @@ -9,8 +9,6 @@ import androidx.work.Configuration import androidx.work.Constraints import androidx.work.Data import androidx.work.NetworkType -import androidx.work.PeriodicWorkRequest -import androidx.work.WorkManager import androidx.work.testing.SynchronousExecutor import androidx.work.testing.WorkManagerTestInitHelper import com.google.common.truth.Truth.assertThat @@ -25,8 +23,8 @@ import org.junit.runner.RunWith import org.oppia.android.domain.oppialogger.EventLogStorageCacheSize import org.oppia.android.domain.oppialogger.ExceptionLogStorageCacheSize import org.oppia.android.domain.oppialogger.OppiaLogger -import org.oppia.android.domain.oppialogger.analytics.AnalyticsController import org.oppia.android.domain.oppialogger.exceptions.ExceptionsController +import org.oppia.android.domain.testing.oppialogger.loguploader.FakeLogUploader import org.oppia.android.testing.FakeEventLogger import org.oppia.android.testing.FakeExceptionLogger import org.oppia.android.testing.TestCoroutineDispatchers @@ -41,10 +39,8 @@ import org.oppia.android.util.logging.LogUploader import org.oppia.android.util.networking.NetworkConnectionUtil import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import java.util.UUID import javax.inject.Inject import javax.inject.Singleton -import kotlin.collections.last @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @@ -57,9 +53,6 @@ class LogUploadWorkManagerInitializerTest { @Inject lateinit var logUploadWorkManagerInitializer: LogUploadWorkManagerInitializer - @Inject - lateinit var analyticsController: AnalyticsController - @Inject lateinit var exceptionsController: ExceptionsController @@ -224,32 +217,3 @@ class LogUploadWorkManagerInitializerTest { fun inject(logUploadWorkRequestTest: LogUploadWorkManagerInitializerTest) } } - -/** A test specific fake for the log uploader. */ -@Singleton -class FakeLogUploader @Inject constructor() : - LogUploader { - - private val eventRequestIdList = mutableListOf() - private val exceptionRequestIdList = mutableListOf() - - override fun enqueueWorkRequestForEvents( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) { - eventRequestIdList.add(workRequest.id) - } - - override fun enqueueWorkRequestForExceptions( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) { - exceptionRequestIdList.add(workRequest.id) - } - - /** Returns the most recent work request id that's stored in the [eventRequestIdList]. */ - fun getMostRecentEventRequestId() = eventRequestIdList.last() - - /** Returns the most recent work request id that's stored in the [exceptionRequestIdList]. */ - fun getMostRecentExceptionRequestId() = exceptionRequestIdList.last() -} diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerTest.kt index 6bc1591211e..b81b1e2eb49 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerTest.kt @@ -27,9 +27,8 @@ import org.oppia.android.domain.oppialogger.EventLogStorageCacheSize import org.oppia.android.domain.oppialogger.ExceptionLogStorageCacheSize import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.oppialogger.analytics.AnalyticsController -import org.oppia.android.domain.oppialogger.analytics.TEST_TIMESTAMP -import org.oppia.android.domain.oppialogger.analytics.TEST_TOPIC_ID import org.oppia.android.domain.oppialogger.exceptions.ExceptionsController +import org.oppia.android.domain.testing.oppialogger.loguploader.FakeLogUploader import org.oppia.android.testing.FakeEventLogger import org.oppia.android.testing.FakeExceptionLogger import org.oppia.android.testing.TestCoroutineDispatchers @@ -47,6 +46,9 @@ import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +private const val TEST_TIMESTAMP = 1556094120000 +private const val TEST_TOPIC_ID = "test_topicId" + @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) From a3fa326c91f135eb1b5d364468b913479e1c9490 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 15 Dec 2020 21:50:03 -0800 Subject: [PATCH 056/289] Gitignore + fix broken test. The test failure here only happens when using JDK9+ (which doesn't happen in the Gradle world, or on CI). The .gitignore is since we can't yet specify a .bazelproject in a shareable way. --- .gitignore | 1 + .../loguploader/LogUploadWorkerTest.kt | 29 +++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index fe10fc61e64..7575c3465f8 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ gradlew.bat !*.secret config/oppia-dev-workflow-remote-cache-credentials.json bazel-* +.bazelproject diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerTest.kt index b81b1e2eb49..ef0f74dbcfd 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerTest.kt @@ -157,13 +157,32 @@ class LogUploadWorkerTest { .build() workManager.enqueue(request) testCoroutineDispatchers.runCurrent() - val workInfo = workManager.getWorkInfoById(request.id) - val exceptionGot = fakeExceptionLogger.getMostRecentException() + val workInfo = workManager.getWorkInfoById(request.id) + val loggedException = fakeExceptionLogger.getMostRecentException() + val loggedExceptionStackTraceElems = loggedException.stackTrace.extractRelevantDetails() + val expectedExceptionStackTraceElems = exception.stackTrace.extractRelevantDetails() assertThat(workInfo.get().state).isEqualTo(WorkInfo.State.SUCCEEDED) - assertThat(exceptionGot.message).isEqualTo("TEST") - assertThat(exceptionGot.stackTrace).isEqualTo(exception.stackTrace) - assertThat(exceptionGot.cause).isEqualTo(null) + assertThat(loggedException.message).isEqualTo("TEST") + assertThat(loggedException.cause).isEqualTo(null) + // The following can't be an exact match for the stack trace since new properties are added to + // stack trace elements in newer versions of Java (such as module name). + assertThat(loggedExceptionStackTraceElems).isEqualTo(expectedExceptionStackTraceElems) + } + + /** + * Returns a list of lists of each relevant element of a [StackTraceElement] to be used for + * comparison in a way that's consistent across JDK versions. + */ + private fun Array.extractRelevantDetails(): List> { + return this.map { element -> + return@map listOf( + element.fileName, + element.methodName, + element.lineNumber, + element.className + ) + } } private fun setUpTestApplicationComponent() { From 33d62aaa415bcab92dbecd9d64eda199f2676532 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 15 Dec 2020 21:53:39 -0800 Subject: [PATCH 057/289] Remove now-ignored .bazelproject. --- .bazelproject | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 .bazelproject diff --git a/.bazelproject b/.bazelproject deleted file mode 100644 index a4dc7774572..00000000000 --- a/.bazelproject +++ /dev/null @@ -1,12 +0,0 @@ -directories: - . - -derive_targets_from_directories: true - -additional_languages: - kotlin - -android_sdk_platform: android-29 - -#bazel_binary: /usr/local/google/home/bhenning/bazel-dev2/bazel-bin/src/bazel -bazel_binary: /usr/local/google/home/bhenning/patched-bazel/bazel From 5908e770af99a81e7b76728b4813aa34d323e364 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 15 Dec 2020 21:56:37 -0800 Subject: [PATCH 058/289] Lint fixes. --- .../testing/oppialogger/loguploader/FakeLogUploader.kt | 10 +++++----- .../oppialogger/loguploader/LogUploadWorkerTest.kt | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/FakeLogUploader.kt b/domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/FakeLogUploader.kt index e9c399663f9..b2a02324827 100644 --- a/domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/FakeLogUploader.kt +++ b/domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/FakeLogUploader.kt @@ -9,20 +9,20 @@ import javax.inject.Singleton /** A test specific fake for the log uploader. */ @Singleton -class FakeLogUploader @Inject constructor(): LogUploader { +class FakeLogUploader @Inject constructor() : LogUploader { private val eventRequestIdList = mutableListOf() private val exceptionRequestIdList = mutableListOf() override fun enqueueWorkRequestForEvents( - workManager: WorkManager, - workRequest: PeriodicWorkRequest + workManager: WorkManager, + workRequest: PeriodicWorkRequest ) { eventRequestIdList.add(workRequest.id) } override fun enqueueWorkRequestForExceptions( - workManager: WorkManager, - workRequest: PeriodicWorkRequest + workManager: WorkManager, + workRequest: PeriodicWorkRequest ) { exceptionRequestIdList.add(workRequest.id) } diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerTest.kt index ef0f74dbcfd..d190118623c 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerTest.kt @@ -177,10 +177,10 @@ class LogUploadWorkerTest { private fun Array.extractRelevantDetails(): List> { return this.map { element -> return@map listOf( - element.fileName, - element.methodName, - element.lineNumber, - element.className + element.fileName, + element.methodName, + element.lineNumber, + element.className ) } } From 654c27a724b950038ac53b6e6e27112dd6893e2a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 7 Jan 2021 15:44:13 -0800 Subject: [PATCH 059/289] Post-merge clean-up. --- .../oppia/android/domain/oppialogger/OppiaLoggerTest.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/OppiaLoggerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/OppiaLoggerTest.kt index eaca5c7c98d..6fa6b2b6701 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/OppiaLoggerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/OppiaLoggerTest.kt @@ -32,14 +32,6 @@ import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -private const val TEST_TOPIC_ID = "test_topicId" -private const val TEST_STORY_ID = "test_storyId" -private const val TEST_EXPLORATION_ID = "test_explorationId" -private const val TEST_QUESTION_ID = "test_questionId" -private const val TEST_SKILL_ID = "test_skillId" -private const val TEST_SKILL_LIST_ID = "test_skillListId" -private const val TEST_SUB_TOPIC_ID = 1 - @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) From 32a6009f55f11641f20933fe77a94c12e55e3691 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 7 Jan 2021 17:11:25 -0800 Subject: [PATCH 060/289] Fix broken post-merge test. --- .../android/domain/oppialogger/OppiaLoggerTest.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/OppiaLoggerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/OppiaLoggerTest.kt index 6fa6b2b6701..2bc45e1b733 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/OppiaLoggerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/OppiaLoggerTest.kt @@ -13,13 +13,6 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.EventLog -import org.oppia.android.domain.oppialogger.analytics.TEST_EXPLORATION_ID -import org.oppia.android.domain.oppialogger.analytics.TEST_QUESTION_ID -import org.oppia.android.domain.oppialogger.analytics.TEST_SKILL_ID -import org.oppia.android.domain.oppialogger.analytics.TEST_SKILL_LIST_ID -import org.oppia.android.domain.oppialogger.analytics.TEST_STORY_ID -import org.oppia.android.domain.oppialogger.analytics.TEST_SUB_TOPIC_ID -import org.oppia.android.domain.oppialogger.analytics.TEST_TOPIC_ID import org.oppia.android.testing.RobolectricModule import org.oppia.android.testing.TestDispatcherModule import org.oppia.android.testing.TestLogReportingModule @@ -32,6 +25,14 @@ import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +private const val TEST_TOPIC_ID = "test_topicId" +private const val TEST_STORY_ID = "test_storyId" +private const val TEST_EXPLORATION_ID = "test_explorationId" +private const val TEST_QUESTION_ID = "test_questionId" +private const val TEST_SKILL_ID = "test_skillId" +private const val TEST_SKILL_LIST_ID = "test_skillListId" +private const val TEST_SUB_TOPIC_ID = 1 + @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) From f3cb6f56541e1d6c8c4fc9082b0464ae9581403a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 7 Jan 2021 23:20:50 -0800 Subject: [PATCH 061/289] Remove unnecessary codeowners per earlier codeowners setup. --- .github/CODEOWNERS | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6b00aaec690..f8172dfdf16 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,10 +11,6 @@ # (See oppia/#10250 for an example.) Please make sure to restore ownership after # the above date passes. -# GitHub Actions & CI workflows. -.github/*.yaml @BenHenning -.github/*.yml @BenHenning - # Blanket codeowners # This is for the case when new files are created in any directories that aren't # covered as a whole, since in these cases, codeowners are not recognized for From 8d4eb873ca69821cb735700e5d1a96d52811f084 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 7 Jan 2021 23:23:42 -0800 Subject: [PATCH 062/289] Fix ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest. --- domain/BUILD.bazel | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index efda03a6067..f43696b92c5 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -298,6 +298,15 @@ domain_test( deps = TEST_DEPS, ) +domain_test( + name = "ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest", + srcs = [ + "src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest.kt", + ], + test_class = "org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest", + deps = TEST_DEPS, +) + domain_test( name = "MultipleChoiceInputEqualsRuleClassifierProviderTest", srcs = [ @@ -343,15 +352,6 @@ domain_test( deps = TEST_DEPS, ) -domain_test( - name = "ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest", - srcs = [ - "src/test/java/org/oppia/android/domain/classify/rules/textinput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest.kt", - ], - test_class = "org.oppia.android.domain.classify.rules.textinput.ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest", - deps = TEST_DEPS, -) - domain_test( name = "TextInputEqualsRuleClassifierProviderTest", srcs = [ From 59268611867d2db6bb12700a3c5a2cd18d62b081 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 7 Jan 2021 23:52:19 -0800 Subject: [PATCH 063/289] Disable image region selection tests. These tests can't correctly pass on Robolectric until #1611 is fixed, so disabling them for the time being to avoid the image loading flake happening on CI (but not locally). Note that chances are a fix will still be needed for the flake, but that can be addressed later. --- .../android/app/player/state/StateFragmentTest.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt index 4db6235e830..d7fbe92e71e 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt @@ -582,6 +582,7 @@ class StateFragmentTest { } @Test + @RunOn(TestPlatform.ESPRESSO) // TODO(#1611): Enable for Robolectric. fun testStateFragment_loadImageRegion_submitButtonDisabled() { launchForExploration(TEST_EXPLORATION_ID_5).use { startPlayingExploration() @@ -595,7 +596,7 @@ class StateFragmentTest { @Test @RunOn(TestPlatform.ESPRESSO) // TODO(#1611): Enable for Robolectric. - fun loadImageRegion_defaultRegionClick_defaultRegionClicked_submitButtonDisabled() { + fun testStateFragment_loadImageRegion_defaultRegionClick_defRegionClicked_submitButtonDisabled() { launchForExploration(TEST_EXPLORATION_ID_5).use { startPlayingExploration() waitForImageViewInteractionToFullyLoad() @@ -608,6 +609,7 @@ class StateFragmentTest { } @Test + @RunOn(TestPlatform.ESPRESSO) // TODO(#1611): Enable for Robolectric. fun testStateFragment_loadImageRegion_clickedRegion6_region6Clicked_submitButtonEnabled() { launchForExploration(TEST_EXPLORATION_ID_5).use { startPlayingExploration() @@ -621,6 +623,7 @@ class StateFragmentTest { } @Test + @RunOn(TestPlatform.ESPRESSO) // TODO(#1611): Enable for Robolectric. fun testStateFragment_loadImageRegion_clickedRegion6_region6Clicked_correctFeedback() { launchForExploration(TEST_EXPLORATION_ID_5).use { startPlayingExploration() @@ -639,6 +642,7 @@ class StateFragmentTest { } @Test + @RunOn(TestPlatform.ESPRESSO) // TODO(#1611): Enable for Robolectric. fun testStateFragment_loadImageRegion_clickedRegion6_region6Clicked_correctAnswer() { launchForExploration(TEST_EXPLORATION_ID_5).use { startPlayingExploration() @@ -657,6 +661,7 @@ class StateFragmentTest { } @Test + @RunOn(TestPlatform.ESPRESSO) // TODO(#1611): Enable for Robolectric. fun testStateFragment_loadImageRegion_clickedRegion6_region6Clicked_continueButtonIsDisplayed() { launchForExploration(TEST_EXPLORATION_ID_5).use { startPlayingExploration() @@ -671,7 +676,8 @@ class StateFragmentTest { } @Test - fun loadImageRegion_clickRegion6_clickedRegion5_region5Clicked_correctFeedback() { + @RunOn(TestPlatform.ESPRESSO) // TODO(#1611): Enable for Robolectric. + fun testStateFragment_loadImageRegion_clickRegion6_clickedRegion5_clickRegion5_correctFeedback() { launchForExploration(TEST_EXPLORATION_ID_5).use { startPlayingExploration() waitForImageViewInteractionToFullyLoad() From b4cd6b464a002a6687f69ff8d1dc590ef2e95e44 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 8 Jan 2021 00:28:48 -0800 Subject: [PATCH 064/289] Disable 2 previously missed tests. --- .../org/oppia/android/app/player/state/StateFragmentTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt index d7fbe92e71e..2acd1babf49 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt @@ -551,6 +551,7 @@ class StateFragmentTest { } @Test + @RunOn(TestPlatform.ESPRESSO) // TODO(#1611): Enable for Robolectric. fun testStateFragment_loadImageRegion_clickRegion6_submitButtonClickable() { launchForExploration(TEST_EXPLORATION_ID_5).use { startPlayingExploration() @@ -564,6 +565,7 @@ class StateFragmentTest { } @Test + @RunOn(TestPlatform.ESPRESSO) // TODO(#1611): Enable for Robolectric. fun testStateFragment_loadImageRegion_clickRegion6_clickSubmit_receivesCorrectFeedback() { launchForExploration(TEST_EXPLORATION_ID_5).use { startPlayingExploration() From eae966094f65539c8bba4472425794d3c8b3c2f7 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 8 Feb 2021 14:45:10 -0800 Subject: [PATCH 065/289] Post-merge lint fix. --- .../org/oppia/android/app/player/state/StateFragmentLocalTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt index cacad853d20..0fc83861a78 100644 --- a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt @@ -21,7 +21,6 @@ import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions.scrollToHolder import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition import androidx.test.espresso.matcher.RootMatchers.isDialog -import androidx.test.espresso.matcher.ViewMatchers.hasChildCount import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isRoot From a8bd5b371a537aa3253a4d5443baa6c151b9077b Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 8 Feb 2021 16:43:32 -0800 Subject: [PATCH 066/289] Add missing dependency. Verified all tests build & pass both for JDK 9 & 11. Hopefully they will work as expected on CI. --- testing/BUILD.bazel | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/BUILD.bazel b/testing/BUILD.bazel index 5ba2534e179..1491038f0c0 100644 --- a/testing/BUILD.bazel +++ b/testing/BUILD.bazel @@ -20,6 +20,7 @@ kt_android_library( ":dagger", "//domain", "//utility", + artifact("nl.dionsegijn:konfetti"), artifact("org.jetbrains.kotlinx:kotlinx-coroutines-test"), artifact("org.robolectric:robolectric"), artifact("org.jetbrains.kotlin:kotlin-reflect"), @@ -84,9 +85,9 @@ testing_test( kt_android_library( name = "assertion_helpers", + testonly = True, srcs = ["src/main/java/org/oppia/android/testing/AssertionHelpers.kt"], visibility = ["//visibility:public"], - testonly = True, deps = [ artifact("org.jetbrains.kotlin:kotlin-reflect"), artifact("junit:junit:4.12"), From 33d4c379917cef2f6784538cebace92e1435d47f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 8 Feb 2021 16:55:50 -0800 Subject: [PATCH 067/289] Add missing codeowners for new files. --- .github/CODEOWNERS | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 54596a3ba2a..5c57263b1c7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -45,6 +45,10 @@ gradlew.bat @BenHenning /.github/*.md @BenHenning /.github/ISSUE_TEMPLATE @BenHenning +# Git secret files & related configurations. +/.gitsecret/ @BenHenning +*.secret @BenHenning + # CI configuration. /.github/workflows/ @BenHenning From b44ae3abddcbfa6fa295e711f3dde8efca44fa84 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 11 Feb 2021 15:09:37 -0800 Subject: [PATCH 068/289] Post-merge fixes. This fixes some tests that were broken after recent PRs, and fixed a visibility error introduced in #2663. --- domain/BUILD.bazel | 9 +-------- testing/BUILD.bazel | 8 ++++++++ third_party/BUILD.bazel | 1 + 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index cb5c9f56c42..aebc7016253 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -31,7 +31,7 @@ kt_android_library( srcs = glob(["src/main/java/org/oppia/android/domain/testing/**/*.kt"]), deps = [ ":domain", - artifact("androidx.work:work-runtime-ktx:2.4.0"), + "//third_party:androidx_work_work-runtime-ktx", ], ) @@ -456,13 +456,6 @@ domain_test( deps = TEST_DEPS, ) -domain_test( - name = "StoryProgressTestHelperTest", - srcs = ["src/test/java/org/oppia/android/domain/topic/StoryProgressTestHelperTest.kt"], - test_class = "org.oppia.android.domain.topic.StoryProgressTestHelperTest", - deps = TEST_DEPS, -) - domain_test( name = "TopicControllerTest", srcs = ["src/test/java/org/oppia/android/domain/topic/TopicControllerTest.kt"], diff --git a/testing/BUILD.bazel b/testing/BUILD.bazel index 02928f48dce..2ad57cd6a11 100644 --- a/testing/BUILD.bazel +++ b/testing/BUILD.bazel @@ -11,6 +11,7 @@ load("//testing:testing_test.bzl", "testing_test") # Library for general-purpose testing fakes. kt_android_library( name = "testing", + testonly = True, srcs = glob(["src/main/java/org/oppia/android/testing/**/*.kt"]), custom_package = "org.oppia.android.testing", manifest = "src/main/AndroidManifest.xml", @@ -83,6 +84,13 @@ testing_test( deps = TEST_DEPS, ) +testing_test( + name = "StoryProgressTestHelperTest", + srcs = ["src/test/java/org/oppia/android/testing/story/StoryProgressTestHelperTest.kt"], + test_class = "org.oppia.android.testing.story.StoryProgressTestHelperTest", + deps = TEST_DEPS, +) + testing_test( name = "FakeOppiaClockTest", srcs = ["src/test/java/org/oppia/android/testing/time/FakeOppiaClockTest.kt"], diff --git a/third_party/BUILD.bazel b/third_party/BUILD.bazel index 159685a3c55..7fcf410315b 100644 --- a/third_party/BUILD.bazel +++ b/third_party/BUILD.bazel @@ -46,6 +46,7 @@ android_library( android_library( name = "robolectric_android-all", + visibility = ["//visibility:public"], exports = [ "@robolectric//bazel:android-all", ], From 1a3f4e94ce3e1ed0b1774ff8035500b73e028aa6 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 11 Feb 2021 15:12:12 -0800 Subject: [PATCH 069/289] Move Bazel tests to new workflow. This will make it easier to restart failures without having to also restart unrelated tests. --- .github/workflows/main.yml | 106 --------------------------- .github/workflows/unit_tests.yml | 118 +++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 106 deletions(-) create mode 100644 .github/workflows/unit_tests.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 71bb1893852..689631594ec 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -221,109 +221,3 @@ jobs: with: name: oppia-bazel-apk path: /home/runner/work/oppia-android/oppia-android/oppia.apk - - bazel_compute_affected_targets: - name: Compute affected tests - runs-on: ubuntu-18.04 - outputs: - matrix: ${{ steps.compute-test-matrix-from-affected.outputs.matrix || steps.compute-test-matrix-from-all.outputs.matrix }} - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Clone Oppia Bazel - run: git clone https://github.com/oppia/bazel.git $HOME/oppia-bazel - - name: Unzip Bazel binary - run: | - cd $HOME/oppia-bazel - unzip bazel-build.zip - cd $GITHUB_WORKSPACE - chmod a+x $HOME/oppia-bazel/bazel - - name: Compute test matrix based on affected targets - id: compute-test-matrix-from-affected - if: "!contains(github.event.pull_request.title, '[RunAllTests]')" - # https://unix.stackexchange.com/a/338124 for reference on creating a JSON-friendly - # comma-separated list of test targets for the matrix. - run: | - TEST_TARGET_LIST=$(bash ./scripts/compute_affected_tests.sh $HOME/oppia-bazel/bazel | sed 's/^\|$/"/g' | paste -sd, -) - echo "Affected tests (note that this might be all tests if on the develop branch): $TEST_TARGET_LIST" - echo "::set-output name=matrix::{\"test-target\":[$TEST_TARGET_LIST]}" - - name: Compute test matrix based on all tests - id: compute-test-matrix-from-all - if: "contains(github.event.pull_request.title, '[RunAllTests]')" - run: | - TEST_TARGET_LIST=$($HOME/oppia-bazel/bazel query "kind(test, //...)" | sed 's/^\|$/"/g' | paste -sd, -) - echo "Affected tests (note that this might be all tests if on the develop branch): $TEST_TARGET_LIST" - echo "::set-output name=matrix::{\"test-target\":[$TEST_TARGET_LIST]}" - - bazel_run_test: - name: Run Bazel Test - needs: bazel_compute_affected_targets - runs-on: ubuntu-18.04 - strategy: - fail-fast: false - matrix: ${{fromJson(needs.bazel_compute_affected_targets.outputs.matrix)}} - steps: - - uses: actions/checkout@v2 - - name: Clone Oppia Bazel - run: git clone https://github.com/oppia/bazel.git $HOME/oppia-bazel - - name: Set up JDK 9 - uses: actions/setup-java@v1 - with: - java-version: 9 - - name: Extract Android tools - run: | - mkdir -p $GITHUB_WORKSPACE/tmp/android_tools - cd $HOME/oppia-bazel - unzip bazel-tools.zip - tar -xf $HOME/oppia-bazel/android_tools.tar.gz -C $GITHUB_WORKSPACE/tmp/android_tools - # See explanation in bazel_build_app for how this is installed. - - name: Install git-secret (non-fork only) - if: github.repository == 'oppia/oppia-android' - shell: bash - run: | - cd $HOME - mkdir -p $HOME/gitsecret - git clone https://github.com/sobolevn/git-secret.git git-secret - cd git-secret && make build - PREFIX="$HOME/gitsecret" make install - echo "$HOME/gitsecret" >> $GITHUB_PATH - echo "$HOME/gitsecret/bin" >> $GITHUB_PATH - - name: Decrypt secrets (non-fork only) - if: github.repository == 'oppia/oppia-android' - env: - GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }} - run: | - cd $HOME - # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout! - echo $GIT_SECRET_GPG_PRIVATE_KEY | base64 --decode > ./git_secret_private_key.gpg - gpg --import ./git_secret_private_key.gpg - cd $GITHUB_WORKSPACE - git secret reveal - - name: Unzip Bazel binary - run: | - cd $HOME/oppia-bazel - unzip bazel-build.zip - cd $GITHUB_WORKSPACE - chmod a+x $HOME/oppia-bazel/bazel - - name: Run Oppia Test (with caching, non-fork only) - if: github.repository == 'oppia/oppia-android' - env: - BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} - run: $HOME/oppia-bazel/bazel test --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- ${{ matrix.test-target }} - - name: Run Oppia Test (without caching, fork only) - if: github.repository != 'oppia/oppia-android' - env: - BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} - run: $HOME/oppia-bazel/bazel test --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools -- ${{ matrix.test-target }} - - # Reference: https://github.community/t/127354/7. - check_test_results: - name: Check Bazel Test Results - needs: bazel_run_test - if: ${{ always() }} - runs-on: ubuntu-18.04 - steps: - - name: Check tests passed - if: ${{ needs.bazel_run_test.result != 'success' }} - run: exit 1 diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml new file mode 100644 index 00000000000..fa5a270d9e9 --- /dev/null +++ b/.github/workflows/unit_tests.yml @@ -0,0 +1,118 @@ +name: Unit Tests (Robolectric - Bazel) + +# Controls when the action will run. Triggers the workflow on pull request +# events or push events in the develop branch. +on: + workflow_dispatch: + pull_request: + push: + branches: + # Push events on develop branch + - develop + +jobs: + bazel_compute_affected_targets: + name: Compute affected tests + runs-on: ubuntu-18.04 + outputs: + matrix: ${{ steps.compute-test-matrix-from-affected.outputs.matrix || steps.compute-test-matrix-from-all.outputs.matrix }} + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Clone Oppia Bazel + run: git clone https://github.com/oppia/bazel.git $HOME/oppia-bazel + - name: Unzip Bazel binary + run: | + cd $HOME/oppia-bazel + unzip bazel-build.zip + cd $GITHUB_WORKSPACE + chmod a+x $HOME/oppia-bazel/bazel + - name: Compute test matrix based on affected targets + id: compute-test-matrix-from-affected + if: "!contains(github.event.pull_request.title, '[RunAllTests]')" + # https://unix.stackexchange.com/a/338124 for reference on creating a JSON-friendly + # comma-separated list of test targets for the matrix. + run: | + TEST_TARGET_LIST=$(bash ./scripts/compute_affected_tests.sh $HOME/oppia-bazel/bazel | sed 's/^\|$/"/g' | paste -sd, -) + echo "Affected tests (note that this might be all tests if on the develop branch): $TEST_TARGET_LIST" + echo "::set-output name=matrix::{\"test-target\":[$TEST_TARGET_LIST]}" + - name: Compute test matrix based on all tests + id: compute-test-matrix-from-all + if: "contains(github.event.pull_request.title, '[RunAllTests]')" + run: | + TEST_TARGET_LIST=$($HOME/oppia-bazel/bazel query "kind(test, //...)" | sed 's/^\|$/"/g' | paste -sd, -) + echo "Affected tests (note that this might be all tests if on the develop branch): $TEST_TARGET_LIST" + echo "::set-output name=matrix::{\"test-target\":[$TEST_TARGET_LIST]}" + + bazel_run_test: + name: Run Bazel Test + needs: bazel_compute_affected_targets + runs-on: ubuntu-18.04 + strategy: + fail-fast: false + matrix: ${{fromJson(needs.bazel_compute_affected_targets.outputs.matrix)}} + steps: + - uses: actions/checkout@v2 + - name: Clone Oppia Bazel + run: git clone https://github.com/oppia/bazel.git $HOME/oppia-bazel + - name: Set up JDK 9 + uses: actions/setup-java@v1 + with: + java-version: 9 + - name: Extract Android tools + run: | + mkdir -p $GITHUB_WORKSPACE/tmp/android_tools + cd $HOME/oppia-bazel + unzip bazel-tools.zip + tar -xf $HOME/oppia-bazel/android_tools.tar.gz -C $GITHUB_WORKSPACE/tmp/android_tools + # See explanation in bazel_build_app for how this is installed. + - name: Install git-secret (non-fork only) + if: github.repository == 'oppia/oppia-android' + shell: bash + run: | + cd $HOME + mkdir -p $HOME/gitsecret + git clone https://github.com/sobolevn/git-secret.git git-secret + cd git-secret && make build + PREFIX="$HOME/gitsecret" make install + echo "$HOME/gitsecret" >> $GITHUB_PATH + echo "$HOME/gitsecret/bin" >> $GITHUB_PATH + - name: Decrypt secrets (non-fork only) + if: github.repository == 'oppia/oppia-android' + env: + GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }} + run: | + cd $HOME + # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout! + echo $GIT_SECRET_GPG_PRIVATE_KEY | base64 --decode > ./git_secret_private_key.gpg + gpg --import ./git_secret_private_key.gpg + cd $GITHUB_WORKSPACE + git secret reveal + - name: Unzip Bazel binary + run: | + cd $HOME/oppia-bazel + unzip bazel-build.zip + cd $GITHUB_WORKSPACE + chmod a+x $HOME/oppia-bazel/bazel + - name: Run Oppia Test (with caching, non-fork only) + if: github.repository == 'oppia/oppia-android' + env: + BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} + run: $HOME/oppia-bazel/bazel test --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- ${{ matrix.test-target }} + - name: Run Oppia Test (without caching, fork only) + if: github.repository != 'oppia/oppia-android' + env: + BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }} + run: $HOME/oppia-bazel/bazel test --override_repository=android_tools=$GITHUB_WORKSPACE/tmp/android_tools -- ${{ matrix.test-target }} + + # Reference: https://github.community/t/127354/7. + check_test_results: + name: Check Bazel Test Results + needs: bazel_run_test + if: ${{ always() }} + runs-on: ubuntu-18.04 + steps: + - name: Check tests passed + if: ${{ needs.bazel_run_test.result != 'success' }} + run: exit 1 From 5832b7369f8b955bed6302312ea5a6685267b199 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 26 Oct 2021 14:53:47 -0700 Subject: [PATCH 070/289] Post-merge fixes. Includes linting & fixing textproto->pb conversion for protos that import protos that import protos. --- app/BUILD.bazel | 12 +- .../oppia/android/app/activity/BUILD.bazel | 2 +- config/config_proto_assets.bzl | 4 +- .../java/org/oppia/android/config/BUILD.bazel | 4 +- data/BUILD.bazel | 2 +- .../android/data/backends/gae/BUILD.bazel | 2 +- .../android/data/persistence/BUILD.bazel | 2 +- domain/BUILD.bazel | 13 +- domain/domain_assets.bzl | 24 +- .../oppia/android/domain/locale/BUILD.bazel | 6 +- .../android/domain/oppialogger/BUILD.bazel | 2 +- .../domain/oppialogger/analytics/BUILD.bazel | 2 +- .../domain/oppialogger/exceptions/BUILD.bazel | 2 +- .../oppia/android/domain/state/BUILD.bazel | 8 +- .../android/domain/translation/BUILD.bazel | 12 +- .../org/oppia/android/domain/util/BUILD.bazel | 7 +- .../android/domain/util/RatioExtensions.kt | 6 +- ...icInputEqualsRuleClassifierProviderTest.kt | 3 +- .../oppia/android/domain/locale/BUILD.bazel | 6 +- .../android/domain/translation/BUILD.bazel | 2 +- model/BUILD.bazel | 282 +----------------- model/oppia_proto_library.bzl | 19 ++ model/src/main/proto/BUILD.bazel | 270 +++++++++++++++++ .../proto/format_import_proto_library.bzl | 45 --- model/src/main/proto/math.proto | 4 +- model/text_proto_assets.bzl | 4 +- scripts/script_assets.bzl | 14 +- .../oppia/android/scripts/common/BUILD.bazel | 2 +- .../oppia/android/testing/junit/BUILD.bazel | 2 +- .../oppia/android/testing/data/BUILD.bazel | 2 +- utility/BUILD.bazel | 41 +-- .../org/oppia/android/util/locale/BUILD.bazel | 4 +- .../oppia/android/util/logging/BUILD.bazel | 4 +- .../android/util/logging/firebase/BUILD.bazel | 4 +- .../org/oppia/android/util/math/BUILD.bazel | 44 +++ .../android/util/math/FractionExtensions.kt | 38 +-- .../util/math/MathExpressionExtensions.kt | 79 ++--- .../android/util/math/MathExpressionParser.kt | 180 +++++------ .../oppia/android/util/math/MathTokenizer.kt | 43 +-- .../android/util/caching/testing/BUILD.bazel | 2 +- .../org/oppia/android/util/locale/BUILD.bazel | 2 +- .../org/oppia/android/util/math/BUILD.bazel | 46 +++ .../util/math/MathExpressionParserTest.kt | 42 +-- .../android/util/math/MathTokenizerTest.kt | 20 +- 44 files changed, 656 insertions(+), 658 deletions(-) create mode 100644 model/oppia_proto_library.bzl create mode 100644 model/src/main/proto/BUILD.bazel delete mode 100644 model/src/main/proto/format_import_proto_library.bzl create mode 100644 utility/src/main/java/org/oppia/android/util/math/BUILD.bazel create mode 100644 utility/src/test/java/org/oppia/android/util/math/BUILD.bazel diff --git a/app/BUILD.bazel b/app/BUILD.bazel index caf40076824..ef235051fe3 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -545,8 +545,8 @@ android_library( ":views", "//app/src/main/java/org/oppia/android/app/translation:app_language_activity_injector_provider", "//app/src/main/java/org/oppia/android/app/translation:app_language_resource_handler", - "//model:interaction_object_java_proto_lite", - "//model:thumbnail_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:thumbnail_java_proto_lite", "//third_party:androidx_annotation_annotation", "//third_party:androidx_constraintlayout_constraintlayout", "//third_party:androidx_core_core", @@ -577,8 +577,8 @@ kt_android_library( custom_package = "org.oppia.android.app", deps = [ ":dagger", - "//model:question_java_proto_lite", - "//model:topic_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", + "//model/src/main/proto:topic_java_proto_lite", "//third_party:androidx_recyclerview_recyclerview", ], ) @@ -677,7 +677,7 @@ android_library( ":view_models", "//app/src/main/java/org/oppia/android/app/translation:app_language_activity_injector_provider", "//app/src/main/java/org/oppia/android/app/translation:app_language_resource_handler", - "//model:thumbnail_java_proto_lite", + "//model/src/main/proto:thumbnail_java_proto_lite", "//third_party:androidx_annotation_annotation", "//third_party:androidx_constraintlayout_constraintlayout", "//third_party:androidx_lifecycle_lifecycle-livedata-core", @@ -728,7 +728,7 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/oppialogger:startup_listener", "//domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions:logger_module", "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:worker_module", - "//model:arguments_java_proto_lite", + "//model/src/main/proto:arguments_java_proto_lite", "//app/src/main/java/org/oppia/android/app/testing/activity:test_activity", "//third_party:androidx_databinding_databinding-adapters", "//third_party:androidx_databinding_databinding-common", diff --git a/app/src/main/java/org/oppia/android/app/activity/BUILD.bazel b/app/src/main/java/org/oppia/android/app/activity/BUILD.bazel index 8b959707002..3c09724c453 100644 --- a/app/src/main/java/org/oppia/android/app/activity/BUILD.bazel +++ b/app/src/main/java/org/oppia/android/app/activity/BUILD.bazel @@ -75,6 +75,6 @@ kt_android_library( "//app:app_visibility", ], deps = [ - "//model:profile_java_proto_lite", + "//model/src/main/proto:profile_java_proto_lite", ], ) diff --git a/config/config_proto_assets.bzl b/config/config_proto_assets.bzl index f3073c33163..c45099babd2 100644 --- a/config/config_proto_assets.bzl +++ b/config/config_proto_assets.bzl @@ -23,8 +23,8 @@ def generate_supported_languages_configuration_from_text_proto( names = [supported_language_text_proto_file_name], proto_dep_name = "languages", proto_type_name = "SupportedLanguages", - name_prefix = name, + name_prefix = "supported_languages", asset_dir = "languages", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) diff --git a/config/src/java/org/oppia/android/config/BUILD.bazel b/config/src/java/org/oppia/android/config/BUILD.bazel index 36079416378..8cc9ae4591c 100644 --- a/config/src/java/org/oppia/android/config/BUILD.bazel +++ b/config/src/java/org/oppia/android/config/BUILD.bazel @@ -10,7 +10,7 @@ _SUPPORTED_LANGUAGES_CONFIG_ASSETS = generate_proto_binary_assets( asset_dir = "languages", name_prefix = "supported_languages_config_assets", names = ["supported_languages"], - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_dep_name = "languages", proto_package = "model", proto_type_name = "SupportedLanguages", @@ -21,7 +21,7 @@ _SUPPORTED_REGIONS_CONFIG_ASSETS = generate_proto_binary_assets( asset_dir = "languages", name_prefix = "supported_regions_config_assets", names = ["supported_regions"], - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_dep_name = "languages", proto_package = "model", proto_type_name = "SupportedRegions", diff --git a/data/BUILD.bazel b/data/BUILD.bazel index 3dc0dde487c..18fe1fd9127 100644 --- a/data/BUILD.bazel +++ b/data/BUILD.bazel @@ -14,7 +14,7 @@ TEST_DEPS = [ "//data/src/main/java/org/oppia/android/data/backends/gae:prod_module", "//data/src/main/java/org/oppia/android/data/backends/gae/model", "//data/src/main/java/org/oppia/android/data/persistence:cache_store", - "//model:test_models", + "//model/src/main/proto:test_models", "//testing", "//testing/src/main/java/org/oppia/android/testing/network", "//testing/src/main/java/org/oppia/android/testing/network:test_module", diff --git a/data/src/main/java/org/oppia/android/data/backends/gae/BUILD.bazel b/data/src/main/java/org/oppia/android/data/backends/gae/BUILD.bazel index 991bb9cb694..c5859673b0f 100644 --- a/data/src/main/java/org/oppia/android/data/backends/gae/BUILD.bazel +++ b/data/src/main/java/org/oppia/android/data/backends/gae/BUILD.bazel @@ -17,7 +17,7 @@ kt_android_library( deps = [ ":constants", ":network_config_annotations", - "//model:arguments_java_proto_lite", + "//model/src/main/proto:arguments_java_proto_lite", "//third_party:com_squareup_okhttp3_okhttp", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/extensions:context_extensions", diff --git a/data/src/main/java/org/oppia/android/data/persistence/BUILD.bazel b/data/src/main/java/org/oppia/android/data/persistence/BUILD.bazel index 0ead7ddfd08..db67d9fb798 100644 --- a/data/src/main/java/org/oppia/android/data/persistence/BUILD.bazel +++ b/data/src/main/java/org/oppia/android/data/persistence/BUILD.bazel @@ -12,7 +12,7 @@ kt_android_library( visibility = ["//:oppia_api_visibility"], deps = [ ":dagger", - "//model:profile_java_proto_lite", + "//model/src/main/proto:profile_java_proto_lite", "//utility", "//utility/src/main/java/org/oppia/android/util/data:async_data_subscription_manager", "//utility/src/main/java/org/oppia/android/util/data:async_result", diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index 414d4fc4517..b83ff251064 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -111,13 +111,12 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/util:asset", "//domain/src/main/java/org/oppia/android/domain/util:extensions", "//domain/src/main/java/org/oppia/android/domain/util:retriever", - "//model:exploration_checkpoint_java_proto_lite", - "//model:onboarding_java_proto_lite", - "//model:platform_parameter_java_proto_lite", - "//model:question_java_proto_lite", - "//model:topic_java_proto_lite", + "//model/src/main/proto:exploration_checkpoint_java_proto_lite", + "//model/src/main/proto:onboarding_java_proto_lite", + "//model/src/main/proto:platform_parameter_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", + "//model/src/main/proto:topic_java_proto_lite", "//third_party:androidx_work_work-runtime-ktx", - "//utility:math", "//utility/src/main/java/org/oppia/android/util/caching:topic_list_to_cache", "//utility/src/main/java/org/oppia/android/util/data:data_providers", "//utility/src/main/java/org/oppia/android/util/extensions:bundle_extensions", @@ -149,7 +148,7 @@ kt_android_library( "src/test/java/org/oppia/android/domain/classify/InteractionObjectTestBuilder.kt", ], deps = [ - "//model:question_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", ], ) diff --git a/domain/domain_assets.bzl b/domain/domain_assets.bzl index dff28e34f9e..89a3ae008a3 100644 --- a/domain/domain_assets.bzl +++ b/domain/domain_assets.bzl @@ -32,53 +32,53 @@ def generate_assets_list_from_text_protos( names = topic_list_file_names, proto_dep_name = "topic", proto_type_name = "TopicIdList", - name_prefix = name, + name_prefix = "topic_id_list", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = topic_file_names, proto_dep_name = "topic", proto_type_name = "TopicRecord", - name_prefix = name, + name_prefix = "topic_record", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = subtopic_file_names, proto_dep_name = "topic", proto_type_name = "SubtopicRecord", - name_prefix = name, + name_prefix = "subtopic_record", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = story_file_names, proto_dep_name = "topic", proto_type_name = "StoryRecord", - name_prefix = name, + name_prefix = "story_record", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = skills_file_names, proto_dep_name = "topic", proto_type_name = "ConceptCardList", - name_prefix = name, + name_prefix = "concept_card_list", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = exploration_file_names, proto_dep_name = "exploration", proto_type_name = "Exploration", - name_prefix = name, + name_prefix = "exploration", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) diff --git a/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel index afc2f8b9fca..a62092b664e 100644 --- a/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel @@ -17,7 +17,7 @@ kt_android_library( ":display_locale_impl", ":language_config_retriever", "//domain/src/main/java/org/oppia/android/domain/oppialogger:oppia_logger", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/data:async_result", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale", @@ -64,7 +64,7 @@ kt_android_library( "//domain:domain_testing_visibility", ], deps = [ - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale", ], ) @@ -80,7 +80,7 @@ kt_android_library( deps = [ ":dagger", "//config/src/java/org/oppia/android/config:languages_config", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/caching:annotations", "//utility/src/main/java/org/oppia/android/util/caching:asset_repository", ], diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel index fde3ce56efa..4d42746e1d1 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel @@ -13,7 +13,7 @@ kt_android_library( visibility = ["//:oppia_api_visibility"], deps = [ "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:controller", - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/logging:console_logger", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel index 320025217d9..0166ef20c36 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel @@ -13,7 +13,7 @@ kt_android_library( deps = [ "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//domain/src/main/java/org/oppia/android/domain/oppialogger:storage_module", - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/logging:console_logger", "//utility/src/main/java/org/oppia/android/util/logging:event_logger", diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/BUILD.bazel index 0cd3f1b4a2b..af13183d30c 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/BUILD.bazel @@ -14,7 +14,7 @@ kt_android_library( deps = [ "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//domain/src/main/java/org/oppia/android/domain/oppialogger:storage_module", - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/logging:console_logger", "//utility/src/main/java/org/oppia/android/util/logging:exception_logger", diff --git a/domain/src/main/java/org/oppia/android/domain/state/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/state/BUILD.bazel index 5568e6221f2..aa32d0a3110 100644 --- a/domain/src/main/java/org/oppia/android/domain/state/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/state/BUILD.bazel @@ -11,7 +11,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:exploration_checkpoint_java_proto_lite", + "//model/src/main/proto:exploration_checkpoint_java_proto_lite", ], ) @@ -22,7 +22,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:exploration_checkpoint_java_proto_lite", + "//model/src/main/proto:exploration_checkpoint_java_proto_lite", ], ) @@ -33,7 +33,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:exploration_java_proto_lite", - "//model:question_java_proto_lite", + "//model/src/main/proto:exploration_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel index 4096a7f922f..700a8b0ad15 100644 --- a/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel @@ -13,12 +13,12 @@ kt_android_library( visibility = ["//:oppia_api_visibility"], deps = [ "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller", - "//model:interaction_object_java_proto_lite", - "//model:languages_java_proto_lite", - "//model:profile_java_proto_lite", - "//model:subtitled_html_java_proto_lite", - "//model:subtitled_unicode_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", + "//model/src/main/proto:profile_java_proto_lite", + "//model/src/main/proto:subtitled_html_java_proto_lite", + "//model/src/main/proto:subtitled_unicode_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/data:async_result", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/data:data_providers", diff --git a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel index 923a0a54e53..e2cc8c369b9 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel @@ -21,8 +21,6 @@ kt_android_library( kt_android_library( name = "extensions", srcs = [ - "FloatExtensions.kt", - "FractionExtensions.kt", "InteractionObjectExtensions.kt", "JsonExtensions.kt", "RatioExtensions.kt", @@ -31,8 +29,9 @@ kt_android_library( ], visibility = ["//domain:__subpackages__"], deps = [ - "//model:question_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", "//third_party:androidx_work_work-runtime-ktx", + "//utility/src/main/java/org/oppia/android/util/math:extensions", ], ) @@ -44,7 +43,7 @@ kt_android_library( visibility = ["//domain:__subpackages__"], deps = [ ":extensions", - "//model:question_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/util/RatioExtensions.kt b/domain/src/main/java/org/oppia/android/domain/util/RatioExtensions.kt index 9a698c306ae..43913b48e73 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/RatioExtensions.kt +++ b/domain/src/main/java/org/oppia/android/domain/util/RatioExtensions.kt @@ -1,6 +1,9 @@ package org.oppia.android.domain.util import org.oppia.android.app.model.RatioExpression +import org.oppia.android.util.math.gcd + +// TODO: move to new package. /** * Returns this Ratio in its most simplified form. @@ -9,10 +12,11 @@ fun RatioExpression.toSimplestForm(): List { return if (this.ratioComponentList.contains(0)) { this.ratioComponentList } else { - val gcdComponentResult = this.ratioComponentList.reduce { x, y -> org.oppia.android.util.math.gcd(x, y) } + val gcdComponentResult = this.ratioComponentList.reduce { x, y -> gcd(x, y) } this.ratioComponentList.map { x -> x / gcdComponentResult } } } + /** * Returns this Ratio in string format. * E.g. [1, 2, 3] will yield to 1:2:3 diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt index 1b990e6eba9..ed2a0e2a24a 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt @@ -9,12 +9,11 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.InteractionObject -import org.oppia.android.util.math.FLOAT_EQUALITY_INTERVAL import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.util.FLOAT_EQUALITY_INTERVAL import org.oppia.android.testing.assertThrows +import org.oppia.android.util.math.FLOAT_EQUALITY_INTERVAL import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject diff --git a/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel index 6d992879342..b1a8a187562 100644 --- a/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel +++ b/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel @@ -34,7 +34,7 @@ oppia_android_test( deps = [ ":dagger", "//domain/src/main/java/org/oppia/android/domain/locale:content_locale_impl", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_extensions_truth-liteproto-extension", "//third_party:com_google_truth_truth", @@ -54,7 +54,7 @@ oppia_android_test( ":dagger", "//domain:test_resources", "//domain/src/main/java/org/oppia/android/domain/locale:display_locale_impl", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/time:test_module", "//third_party:androidx_test_ext_junit", @@ -121,7 +121,7 @@ oppia_android_test( deps = [ ":dagger", "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", diff --git a/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel index 0877354b02c..d4733f05076 100644 --- a/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel +++ b/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel @@ -15,7 +15,7 @@ oppia_android_test( ":dagger", "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller", "//domain/src/main/java/org/oppia/android/domain/translation:translation_controller", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", diff --git a/model/BUILD.bazel b/model/BUILD.bazel index aabf6426f74..1755f53361e 100644 --- a/model/BUILD.bazel +++ b/model/BUILD.bazel @@ -1,283 +1,3 @@ -# TODO(#1532): Rename file to 'BUILD' post-Gradle. """ -This library contains all protos used in the app and is a dependency for all other modules. -In Bazel, proto files are built using the proto_library() and java_lite_proto_library() rules. -The proto_library() rule creates a proto file library to be used in multiple languages. -The java_lite_proto_library() rule takes in a proto_library target and generates java code. +TODO: add docs """ - -load("@rules_java//java:defs.bzl", "java_lite_proto_library") -load("@rules_proto//proto:defs.bzl", "proto_library") -load("//model:src/main/proto/format_import_proto_library.bzl", "format_import_proto_library") - -# NOTE TO DEVELOPERS: When adding new protos, each proto will need to have both a proto_library -# and java_lite_proto_library. See the examples below for context. Further, once the proto lite -# library is added, it should be included in the exports list in the model library at the -# bottom of this file so that other parts of the app get access to it. If protos import other -# protos, they need to use format_import_proto_library (again, see examples below for how to do -# this). -# -# For example, if adding a new proto file called 'important_structure.proto', add these: -# proto_library( -# name = "important_structure_proto", -# srcs = ["src/main/proto/important_structure.proto"], -# ) -# -# java_lite_proto_library( -# name = "important_structure_java_proto_lite", -# deps = [":important_structure_proto"], -# ) -# -# And change the 'model' library at the bottom of the file, e.g.: -# android_library( -# name = "model", -# exports = [ -# ... -# ":important_structure_java_proto_lite", -# ... -# ], -# ... -# ) - -proto_library( - name = "arguments_proto", - srcs = ["src/main/proto/arguments.proto"], -) - -java_lite_proto_library( - name = "arguments_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":arguments_proto"], -) - -proto_library( - name = "event_logger_proto", - srcs = ["src/main/proto/oppia_logger.proto"], -) - -java_lite_proto_library( - name = "event_logger_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":event_logger_proto"], -) - -format_import_proto_library( - name = "exploration_checkpoint", - src = "src/main/proto/exploration_checkpoint.proto", - deps = [":exploration_proto"], -) - -java_lite_proto_library( - name = "exploration_checkpoint_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":exploration_checkpoint_proto"], -) - -format_import_proto_library( - name = "interaction_object_proto", - src = "src/main/proto/interaction_object.proto", - deps = [ - ":math_proto", - ], -) - -java_lite_proto_library( - name = "interaction_object_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":interaction_object_proto"], -) - -proto_library( - name = "math_proto", - srcs = ["src/main/proto/math.proto"], -) - -java_lite_proto_library( - name = "math_java_proto_lite", - deps = [":math_proto"], -) - -proto_library( - name = "languages_proto", - srcs = ["src/main/proto/languages.proto"], - visibility = ["//visibility:public"], -) - -java_lite_proto_library( - name = "languages_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":languages_proto"], -) - -proto_library( - name = "onboarding_proto", - srcs = ["src/main/proto/onboarding.proto"], -) - -java_lite_proto_library( - name = "onboarding_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":onboarding_proto"], -) - -proto_library( - name = "profile_proto", - srcs = ["src/main/proto/profile.proto"], -) - -java_lite_proto_library( - name = "profile_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":profile_proto"], -) - -proto_library( - name = "subtitled_html_proto", - srcs = ["src/main/proto/subtitled_html.proto"], -) - -java_lite_proto_library( - name = "subtitled_html_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":subtitled_html_proto"], -) - -proto_library( - name = "subtitled_unicode_proto", - srcs = ["src/main/proto/subtitled_unicode.proto"], -) - -java_lite_proto_library( - name = "subtitled_unicode_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":subtitled_unicode_proto"], -) - -proto_library( - name = "test_proto", - srcs = ["src/main/proto/test.proto"], -) - -java_lite_proto_library( - name = "test_java_proto_lite", - deps = [":test_proto"], -) - -proto_library( - name = "thumbnail_proto", - srcs = ["src/main/proto/thumbnail.proto"], -) - -java_lite_proto_library( - name = "thumbnail_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":thumbnail_proto"], -) - -proto_library( - name = "translation_proto", - srcs = ["src/main/proto/translation.proto"], -) - -java_lite_proto_library( - name = "translation_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":translation_proto"], -) - -proto_library( - name = "voiceover_proto", - srcs = ["src/main/proto/voiceover.proto"], -) - -java_lite_proto_library( - name = "voiceover_java_proto_lite", - deps = [":voiceover_proto"], -) - -format_import_proto_library( - name = "feedback_reporting", - src = "src/main/proto/feedback_reporting.proto", - deps = [ - ":profile_proto", - ], -) - -java_lite_proto_library( - name = "feedback_reporting_java_proto_lite", - deps = [":feedback_reporting_proto"], -) - -format_import_proto_library( - name = "question", - src = "src/main/proto/question.proto", - deps = [ - ":exploration_proto", - ":subtitled_html_proto", - ":subtitled_unicode_proto", - ], -) - -java_lite_proto_library( - name = "question_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":question_proto"], -) - -format_import_proto_library( - name = "topic", - src = "src/main/proto/topic.proto", - visibility = ["//visibility:public"], - deps = [ - ":subtitled_html_proto", - ":subtitled_unicode_proto", - ":thumbnail_proto", - ":translation_proto", - ":voiceover_proto", - ], -) - -java_lite_proto_library( - name = "topic_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":topic_proto"], -) - -format_import_proto_library( - name = "exploration", - src = "src/main/proto/exploration.proto", - visibility = ["//visibility:public"], - deps = [ - ":interaction_object_proto", - ":subtitled_html_proto", - ":subtitled_unicode_proto", - ":translation_proto", - ":voiceover_proto", - ], -) - -java_lite_proto_library( - name = "exploration_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":exploration_proto"], -) - -format_import_proto_library( - name = "platform_parameter", - src = "src/main/proto/platform_parameter.proto", -) - -java_lite_proto_library( - name = "platform_parameter_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":platform_parameter_proto"], -) - -android_library( - name = "test_models", - testonly = True, - visibility = ["//visibility:public"], - exports = [ - ":test_java_proto_lite", - ], -) diff --git a/model/oppia_proto_library.bzl b/model/oppia_proto_library.bzl new file mode 100644 index 00000000000..8f6ac753135 --- /dev/null +++ b/model/oppia_proto_library.bzl @@ -0,0 +1,19 @@ +""" +TODO: add docs +""" + +load("@rules_proto//proto:defs.bzl", "proto_library") + +# TODO: add regex check +# TODO: add TODO to remove +# TODO: maybe close format proto issue with this PR? + +def oppia_proto_library(name, strip_import_prefix = "", **kwargs): + """ + TODO: add docs + """ + proto_library( + name = name, + strip_import_prefix = strip_import_prefix, + **kwargs + ) diff --git a/model/src/main/proto/BUILD.bazel b/model/src/main/proto/BUILD.bazel new file mode 100644 index 00000000000..bcb708d9f5b --- /dev/null +++ b/model/src/main/proto/BUILD.bazel @@ -0,0 +1,270 @@ +# TODO(#1532): Rename file to 'BUILD' post-Gradle. +""" +This library contains all protos used in the app and is a dependency for all other modules. +In Bazel, proto files are built using the oppia_proto_library() and java_lite_proto_library() rules. +The oppia_proto_library() rule creates a proto file library to be used in multiple languages. +The java_lite_proto_library() rule takes in a proto_library target and generates java code. +""" + +load("@rules_java//java:defs.bzl", "java_lite_proto_library") +load("//model:oppia_proto_library.bzl", "oppia_proto_library") + +# NOTE TO DEVELOPERS: When adding new protos, each proto will need to have both a proto_library +# and java_lite_proto_library. +# +# For example, if adding a new proto file called 'important_structure.proto', add these: +# oppia_proto_library( +# name = "important_structure_proto", +# srcs = ["src/main/proto/important_structure.proto"], +# ) +# +# java_lite_proto_library( +# name = "important_structure_java_proto_lite", +# deps = [":important_structure_proto"], +# ) + +oppia_proto_library( + name = "arguments_proto", + srcs = ["arguments.proto"], +) + +java_lite_proto_library( + name = "arguments_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":arguments_proto"], +) + +oppia_proto_library( + name = "event_logger_proto", + srcs = ["oppia_logger.proto"], +) + +java_lite_proto_library( + name = "event_logger_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":event_logger_proto"], +) + +oppia_proto_library( + name = "exploration_checkpoint_proto", + srcs = ["exploration_checkpoint.proto"], + deps = [":exploration_proto"], +) + +java_lite_proto_library( + name = "exploration_checkpoint_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":exploration_checkpoint_proto"], +) + +oppia_proto_library( + name = "interaction_object_proto", + srcs = ["interaction_object.proto"], + visibility = ["//:oppia_api_visibility"], + deps = [":math_proto"], +) + +java_lite_proto_library( + name = "interaction_object_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":interaction_object_proto"], +) + +oppia_proto_library( + name = "math_proto", + srcs = ["math.proto"], + strip_import_prefix = "", +) + +java_lite_proto_library( + name = "math_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":math_proto"], +) + +oppia_proto_library( + name = "languages_proto", + srcs = ["languages.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "languages_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":languages_proto"], +) + +oppia_proto_library( + name = "onboarding_proto", + srcs = ["onboarding.proto"], +) + +java_lite_proto_library( + name = "onboarding_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":onboarding_proto"], +) + +oppia_proto_library( + name = "profile_proto", + srcs = ["profile.proto"], +) + +java_lite_proto_library( + name = "profile_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":profile_proto"], +) + +oppia_proto_library( + name = "subtitled_html_proto", + srcs = ["subtitled_html.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "subtitled_html_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":subtitled_html_proto"], +) + +oppia_proto_library( + name = "subtitled_unicode_proto", + srcs = ["subtitled_unicode.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "subtitled_unicode_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":subtitled_unicode_proto"], +) + +oppia_proto_library( + name = "test_proto", + srcs = ["test.proto"], +) + +java_lite_proto_library( + name = "test_java_proto_lite", + deps = [":test_proto"], +) + +oppia_proto_library( + name = "thumbnail_proto", + srcs = ["thumbnail.proto"], +) + +java_lite_proto_library( + name = "thumbnail_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":thumbnail_proto"], +) + +oppia_proto_library( + name = "translation_proto", + srcs = ["translation.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "translation_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":translation_proto"], +) + +oppia_proto_library( + name = "voiceover_proto", + srcs = ["voiceover.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "voiceover_java_proto_lite", + deps = [":voiceover_proto"], +) + +oppia_proto_library( + name = "feedback_reporting_proto", + srcs = ["feedback_reporting.proto"], + deps = [":profile_proto"], +) + +java_lite_proto_library( + name = "feedback_reporting_java_proto_lite", + deps = [":feedback_reporting_proto"], +) + +oppia_proto_library( + name = "question_proto", + srcs = ["question.proto"], + deps = [ + ":exploration_proto", + ":subtitled_html_proto", + ":subtitled_unicode_proto", + ], +) + +java_lite_proto_library( + name = "question_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":question_proto"], +) + +oppia_proto_library( + name = "topic_proto", + srcs = ["topic.proto"], + visibility = ["//:oppia_api_visibility"], + deps = [ + ":subtitled_html_proto", + ":subtitled_unicode_proto", + ":thumbnail_proto", + ":translation_proto", + ":voiceover_proto", + ], +) + +java_lite_proto_library( + name = "topic_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":topic_proto"], +) + +oppia_proto_library( + name = "exploration_proto", + srcs = ["exploration.proto"], + visibility = ["//:oppia_api_visibility"], + deps = [ + ":interaction_object_proto", + ":subtitled_html_proto", + ":subtitled_unicode_proto", + ":translation_proto", + ":voiceover_proto", + ], +) + +java_lite_proto_library( + name = "exploration_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":exploration_proto"], +) + +oppia_proto_library( + name = "platform_parameter_proto", + srcs = ["platform_parameter.proto"], +) + +java_lite_proto_library( + name = "platform_parameter_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":platform_parameter_proto"], +) + +android_library( + name = "test_models", + testonly = True, + visibility = ["//:oppia_api_visibility"], + exports = [ + ":test_java_proto_lite", + ], +) diff --git a/model/src/main/proto/format_import_proto_library.bzl b/model/src/main/proto/format_import_proto_library.bzl deleted file mode 100644 index f57a4f74fc0..00000000000 --- a/model/src/main/proto/format_import_proto_library.bzl +++ /dev/null @@ -1,45 +0,0 @@ -""" -Container for macros to fix proto files. -""" - -load("@rules_proto//proto:defs.bzl", "proto_library") - -def format_import_proto_library(name, src, deps = [], **kwargs): - """ - Creates a new proto library with corrected imports. - - This macro exists as a way to build proto files that contain import statements in both Gradle - and Bazel. This macro formats the src file's import statements to contain a full path to the - file in order for Bazel to properly locate file. - - Args: - name: str. The name of the .proto file without the '.proto' suffix. This will be the root for - the name of the proto library created. Ex: If name = 'topic', then the src file is - 'topic.proto' and the proto library created will be named 'topic_proto'. - src: str. The name of the .proto file to be built into a proto_library. - deps: list of str. The list of dependencies needed to build the src file. This list will - contain all of the proto_library targets for the files imported into src. - **kwargs: additional parameters passed in. - """ - - # TODO(#1543): Ensure this function works on Windows systems. - # TODO(#1617): Remove genrules post-gradle - native.genrule( - name = name, - srcs = [src], - outs = ["processed_" + src], - cmd = """ - cat $< | - sed 's/import "/import "model\\/src\\/main\\/proto\\//g' | - sed 's/"model\\/src\\/main\\/proto\\/exploration/"model\\/processed_src\\/main\\/proto\\/exploration/g' | - sed 's/"model\\/src\\/main\\/proto\\/topic/"model\\/processed_src\\/main\\/proto\\/topic/g' | - sed 's/"model\\/src\\/main\\/proto\\/question/"model\\/processed_src\\/main\\/proto\\/question/g' > $@ | - sed 's/"model\\/src\\/main\\/proto\\/interaction_object/"model\\/processed_src\\/main\\/proto\\/interaction_object/g' > $@ - """, - ) - proto_library( - name = name + "_proto", - srcs = ["processed_" + src], - deps = deps, - **kwargs - ) diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 121b65430fd..5da8a15b477 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -26,7 +26,7 @@ message MathExpression { message MathBinaryOperation { enum Operator { - OPERATOR_UNKNOWN = 0; + OPERATOR_UNSPECIFIED = 0; // Represents adding two values, e.g.: 1 + x. ADD = 1; // Represents subtracting two values, e.g.: x - 2. @@ -46,7 +46,7 @@ message MathBinaryOperation { message MathUnaryOperation { enum Operator { - OPERATOR_UNKNOWN = 0; + OPERATOR_UNSPECIFIED = 0; // Represents negating a value, e.g.: -y. NEGATE = 1; } diff --git a/model/text_proto_assets.bzl b/model/text_proto_assets.bzl index 326dd00933a..ace61c6a81b 100644 --- a/model/text_proto_assets.bzl +++ b/model/text_proto_assets.bzl @@ -29,9 +29,11 @@ def _gen_binary_proto_from_text_impl(ctx): # proto to binary, and expected stdin/stdout configurations. Note that the actual proto files # are passed to the compiler since it requires them in order to transcode the text proto file. command_path = ctx.executable._protoc_tool.path + proto_directory_path_args = ["--proto_path=%s" % file.dirname for file in input_proto_files] + proto_file_names = [file.basename for file in input_proto_files] arguments = [command_path] + [ "--encode %s" % ctx.attr.proto_type_name, - ] + [file.path for file in input_proto_files] + [ + ] + proto_directory_path_args + proto_file_names + [ "< %s" % input_file, "> %s" % output_file.path, ] diff --git a/scripts/script_assets.bzl b/scripts/script_assets.bzl index b5c3fb389e4..363455fb553 100644 --- a/scripts/script_assets.bzl +++ b/scripts/script_assets.bzl @@ -24,7 +24,7 @@ def generate_regex_assets_list_from_text_protos( names = filepath_pattern_validation_file_names, proto_dep_name = "filename_pattern_validation_checks", proto_type_name = "FilenameChecks", - name_prefix = name, + name_prefix = "filename_checks", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -33,7 +33,7 @@ def generate_regex_assets_list_from_text_protos( names = file_content_validation_file_names, proto_dep_name = "file_content_validation_checks", proto_type_name = "FileContentChecks", - name_prefix = name, + name_prefix = "file_content_checks", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -57,7 +57,7 @@ def generate_test_file_assets_list_from_text_protos( names = test_file_exemptions_name, proto_dep_name = "script_exemptions", proto_type_name = "TestFileExemptions", - name_prefix = name, + name_prefix = "test_file_exemptions", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -82,7 +82,7 @@ def generate_maven_assets_list_from_text_protos( names = maven_dependency_filenames, proto_dep_name = "maven_dependencies", proto_type_name = "MavenDependencyList", - name_prefix = name, + name_prefix = "maven_dependency_list", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -107,7 +107,7 @@ def generate_accessibility_label_assets_list_from_text_protos( names = accessibility_label_exemptions_name, proto_dep_name = "script_exemptions", proto_type_name = "AccessibilityLabelExemptions", - name_prefix = name, + name_prefix = "accessibility_label_exemptions", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -131,7 +131,7 @@ def generate_kdoc_validity_assets_list_from_text_protos( names = kdoc_validity_exemptions_name, proto_dep_name = "script_exemptions", proto_type_name = "KdocValidityExemptions", - name_prefix = name, + name_prefix = "kdoc_validity_exemptions", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -155,7 +155,7 @@ def generate_todo_assets_list_from_text_protos( names = todo_exemptions_name, proto_dep_name = "script_exemptions", proto_type_name = "TodoOpenExemptions", - name_prefix = name, + name_prefix = "todo_open_exemptions", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", diff --git a/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel b/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel index b4559459055..e47fc741ab6 100644 --- a/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel +++ b/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel @@ -43,7 +43,7 @@ kt_jvm_test( name = "ProtoStringEncoderTest", srcs = ["ProtoStringEncoderTest.kt"], deps = [ - "//model:test_models", + "//model/src/main/proto:test_models", "//scripts/src/java/org/oppia/android/scripts/common:proto_string_encoder", "//testing:assertion_helpers", "//third_party:com_google_truth_extensions_truth-liteproto-extension", diff --git a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel index e2c54937b89..e8d62702d99 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel @@ -25,7 +25,7 @@ kt_android_library( ":define_app_language_locale_context", "//domain/src/main/java/org/oppia/android/domain/locale:locale_application_injector", "//domain/src/main/java/org/oppia/android/domain/locale:locale_application_injector_provider", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//third_party:androidx_test_core", "//third_party:junit_junit", ], diff --git a/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel b/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel index 4b84736203b..bfc5a1ab0ac 100644 --- a/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel +++ b/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel @@ -16,7 +16,7 @@ oppia_android_test( ":dagger", "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller", "//domain/src/main/java/org/oppia/android/domain/translation:translation_controller", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", diff --git a/utility/BUILD.bazel b/utility/BUILD.bazel index ce94543a164..513122f87ef 100644 --- a/utility/BUILD.bazel +++ b/utility/BUILD.bazel @@ -55,8 +55,8 @@ kt_android_library( ":resources", "//app:crashlytics", "//app:crashlytics_deps", - "//model:event_logger_java_proto_lite", - "//model:platform_parameter_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", + "//model/src/main/proto:platform_parameter_java_proto_lite", "//third_party:androidx_appcompat_appcompat", "//third_party:androidx_room_room-runtime", "//third_party:androidx_work_work-runtime", @@ -80,23 +80,6 @@ kt_android_library( ], ) -# Utilities specific to mathematics content. -kt_android_library( - name = "math", - srcs = [ - "src/main/java/org/oppia/android/util/math/FloatExtensions.kt", - "src/main/java/org/oppia/android/util/math/FractionExtensions.kt", - "src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt", - "src/main/java/org/oppia/android/util/math/MathExpressionParser.kt", - "src/main/java/org/oppia/android/util/math/MathTokenizer.kt", - ], - custom_package = "org.oppia.android.util.math", - visibility = ["//visibility:public"], - deps = [ - "//model", - ], -) - filegroup( name = "test_manifest", srcs = ["src/test/AndroidManifest.xml"], @@ -109,7 +92,7 @@ TEST_DEPS = [ ":utility", "//app:crashlytics", "//app:crashlytics_deps", - "//model:test_models", + "//model/src/main/proto:test_models", "//testing", "//testing/src/main/java/org/oppia/android/testing/mockito", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", @@ -146,22 +129,4 @@ MIGRATED_TESTS = [ deps = TEST_DEPS, ) for test_file_path in glob(["src/test/java/org/oppia/android/util/**/*Test.kt"])] -utility_test( - name = "MathTokenizerTest", - srcs = ["src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt"], - test_class = "org.oppia.android.util.math.MathTokenizerTest", - deps = TEST_DEPS + [ - ":math", - ], -) - -utility_test( - name = "MathExpressionParserTest", - srcs = ["src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt"], - test_class = "org.oppia.android.util.math.MathExpressionParserTest", - deps = TEST_DEPS + [ - ":math", - ], -) - dagger_rules() diff --git a/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel index 1522fe1a28a..0c57bde04d0 100644 --- a/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel @@ -36,7 +36,7 @@ kt_android_library( visibility = ["//:oppia_api_visibility"], deps = [ ":oppia_locale_context_extensions", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//third_party:androidx_annotation_annotation", ], ) @@ -63,7 +63,7 @@ kt_android_library( "OppiaLocaleContextExtensions.kt", ], deps = [ - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", ], ) diff --git a/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel index 3ea1f8bbe89..21b2fa9154c 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel @@ -39,7 +39,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", ], ) @@ -50,7 +50,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", ], ) diff --git a/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel index 3ba7077b23e..5ed1a9400fa 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel @@ -23,7 +23,7 @@ kt_android_library( "FirebaseLogUploader.kt", ], deps = [ - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//third_party:androidx_work_work-runtime", "//third_party:androidx_work_work-runtime-ktx", "//third_party:com_google_firebase_firebase-analytics", @@ -59,7 +59,7 @@ kt_android_library( "DebugEventLogger.kt", ], deps = [ - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/logging:event_logger", ], diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel new file mode 100644 index 00000000000..db12f3ec1f6 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -0,0 +1,44 @@ +""" +TODO: document +""" + +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "extensions", + srcs = [ + "FloatExtensions.kt", + "FractionExtensions.kt", + "MathExpressionExtensions.kt", + ], + visibility = [ + "//:oppia_api_visibility", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "parser", + srcs = [ + "MathExpressionParser.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":tokenizer", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "tokenizer", + srcs = [ + "MathTokenizer.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], +) diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index 751a2af42e6..cde2eb58bca 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -70,10 +70,10 @@ fun Fraction.toDouble(): Double = toFloat().toDouble() fun Fraction.toSimplestForm(): Fraction { val commonDenominator = gcd(numerator, denominator) return toBuilder() - .setWholeNumber(wholeNumber) - .setNumerator(numerator / commonDenominator) + .setWholeNumber(wholeNumber) + .setNumerator(numerator / commonDenominator) .setDenominator(denominator / commonDenominator) - .build() + .build() } /** @@ -94,9 +94,9 @@ fun Fraction.toImproperForm(): Fraction { fun Fraction.toProperForm(): Fraction { return toSimplestForm().let { it.toBuilder() - .setWholeNumber(it.wholeNumber + (it.numerator / it.denominator)) - .setNumerator(it.numerator % it.denominator) - .build() + .setWholeNumber(it.wholeNumber + (it.numerator / it.denominator)) + .setNumerator(it.numerator % it.denominator) + .build() } } @@ -129,11 +129,11 @@ operator fun Fraction.plus(rhs: Fraction): Fraction { // Finally, compute the new fraction and convert it to proper form to compute its whole number. return Fraction.newBuilder() - .setIsNegative(isNegative) - .setNumerator(newNumerator) - .setDenominator(commonDenominator) - .build() - .toProperForm() + .setIsNegative(isNegative) + .setNumerator(newNumerator) + .setDenominator(commonDenominator) + .build() + .toProperForm() } /** @@ -157,11 +157,11 @@ operator fun Fraction.times(rhs: Fraction): Fraction { // Third, determine negative (negative is retained if only one is negative). val isNegative = leftFraction.isNegative xor rightFraction.isNegative return Fraction.newBuilder() - .setIsNegative(isNegative) - .setNumerator(newNumerator) - .setDenominator(newDenominator) - .build() - .toProperForm() + .setIsNegative(isNegative) + .setNumerator(newNumerator) + .setDenominator(newDenominator) + .build() + .toProperForm() } /** Returns the proper form of the division from this fraction by the specified fraction. */ @@ -174,9 +174,9 @@ operator fun Fraction.div(rhs: Fraction): Fraction { private fun Fraction.toInvertedImproperForm(): Fraction { val improper = toImproperForm() return improper.toBuilder() - .setNumerator(improper.denominator) - .setDenominator(improper.numerator) - .build() + .setNumerator(improper.denominator) + .setDenominator(improper.numerator) + .build() } /** Returns the negated form of this fraction. */ diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index becdfd2dc6e..2418e97551c 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -165,9 +165,9 @@ private fun Polynomial.getConstant(): Real { private operator fun Polynomial.unaryMinus(): Polynomial { // Negating a polynomial just requires flipping the signs on all coefficients. return toBuilder() - .clearTerm() - .addAllTerm(termList.map { it.toBuilder().setCoefficient(-it.coefficient).build() }) - .build() + .clearTerm() + .addAllTerm(termList.map { it.toBuilder().setCoefficient(-it.coefficient).build() }) + .build() } private operator fun Polynomial.plus(rhs: Polynomial): Polynomial { @@ -185,9 +185,11 @@ private operator fun Polynomial.times(rhs: Polynomial): Polynomial { // TODO: ensure this properly computes trivial cases like (x^2 becoming x-squared) or whether // those cases need to be special cased. return Polynomial.newBuilder() - .addAllTerm(termList.flatMap { leftTerm -> + .addAllTerm( + termList.flatMap { leftTerm -> rhs.termList.map { rightTerm -> leftTerm * rightTerm } - }).build() + } + ).build() } private operator fun Term.times(rhs: Term): Term { @@ -210,9 +212,9 @@ private operator fun Term.times(rhs: Term): Term { } return Term.newBuilder() - .setCoefficient(combinedCoefficient) - .addAllVariable(newVariableList) - .build() + .setCoefficient(combinedCoefficient) + .addAllVariable(newVariableList) + .build() } private operator fun Polynomial.div(rhs: Polynomial): Polynomial? { @@ -265,9 +267,9 @@ private operator fun Term.div(rhs: Term): Term? { // Remove variables with powers of 0 since those have been fully divided. Also, divide the // coefficients to finish the division. return Term.newBuilder() - .setCoefficient(coefficient / rhs.coefficient) - .addAllVariable(quotientPowerMap.filter { (_, power) -> power > 0 }.toVariableList()) - .build() + .setCoefficient(coefficient / rhs.coefficient) + .addAllVariable(quotientPowerMap.filter { (_, power) -> power > 0 }.toVariableList()) + .build() } private fun List.toPowerMap(): Map { @@ -341,41 +343,41 @@ private fun MathBinaryOperation.collectChildren(): MutableList - ): ExpressionTreeNode() + val mathExpression: MathExpression, + val children: MutableList + ) : ExpressionTreeNode() - data class PolynomialNode(val polynomial: Polynomial): ExpressionTreeNode() + data class PolynomialNode(val polynomial: Polynomial) : ExpressionTreeNode() - data class ConstantNode(val constant: Real): ExpressionTreeNode() + data class ConstantNode(val constant: Real) : ExpressionTreeNode() } // TODO: add a faster isReducibleToConstant recursive function since this is used a lot. -//private fun MathExpression.reduceToConstant(): MathExpression? { +// private fun MathExpression.reduceToConstant(): MathExpression? { // return when (expressionTypeCase) { // CONSTANT -> this // VARIABLE -> null @@ -383,16 +385,16 @@ private sealed class ExpressionTreeNode { // BINARY_OPERATION -> binaryOperation.reduceToConstant() // else -> null // } -//} +// } -//private fun MathUnaryOperation.reduceToConstant(): MathExpression? { +// private fun MathUnaryOperation.reduceToConstant(): MathExpression? { // return when (operator) { // MathUnaryOperation.Operator.NEGATE -> operand.reduceToConstant()?.transformConstant { -it } // else -> null // } -//} +// } -//private fun MathBinaryOperation.reduceToConstant(): MathExpression? { +// private fun MathBinaryOperation.reduceToConstant(): MathExpression? { // val leftConstant = leftOperand.reduceToConstant()?.constant ?: return null // val rightConstant = rightOperand.reduceToConstant()?.constant ?: return null // return when (operator) { @@ -403,10 +405,10 @@ private sealed class ExpressionTreeNode { // MathBinaryOperation.Operator.EXPONENTIATE -> fromConstant(leftConstant.pow(rightConstant)) // else -> null // } -//} +// } private fun MathExpression.transformConstant( - transform: (Real.Builder) -> Real.Builder + transform: (Real.Builder) -> Real.Builder ): MathExpression { return toBuilder().setConstant(transform(constant.toBuilder())).build() } @@ -430,12 +432,13 @@ private fun Real.recompute(transform: (Real.Builder) -> Real.Builder): Real { } private fun combine( - lhs: Real, - rhs: Real, - leftRationalRightRationalOp: (Fraction, Fraction) -> Fraction, - leftRationalRightIrrationalOp: (Fraction, Double) -> Double, - leftIrrationalRightRationalOp: (Double, Fraction) -> Double, - leftIrrationalRightIrrationalOp: (Double, Double) -> Double): Real { + lhs: Real, + rhs: Real, + leftRationalRightRationalOp: (Fraction, Fraction) -> Fraction, + leftRationalRightIrrationalOp: (Fraction, Double) -> Double, + leftIrrationalRightRationalOp: (Double, Fraction) -> Double, + leftIrrationalRightIrrationalOp: (Double, Double) -> Double +): Real { return when (lhs.realTypeCase) { Real.RealTypeCase.RATIONAL -> { // Left-hand side is Fraction. diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index 473dbe97034..beb8190ed66 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -77,9 +77,10 @@ class MathExpressionParser { val topPrecedence = top.parsedOperator.precedence if (topPrecedence < precedence) break if (isUnaryOperator) break // Unary operators do not pop operators. - if (topPrecedence == precedence - && parsedOperator is ParsedOperator.BinaryOperator - && parsedOperator.associativity != ParsedOperator.Associativity.LEFT) break + if (topPrecedence == precedence && + parsedOperator is ParsedOperator.BinaryOperator && + parsedOperator.associativity != ParsedOperator.Associativity.LEFT + ) break operatorStack.pop() outputQueue += top } @@ -96,11 +97,12 @@ class MathExpressionParser { operatorStack.pop() outputQueue += top } - if (operatorStack.isEmpty() - || operatorStack.peek() !is ParsedToken.Groupable.OpenParenthesis) { + if (operatorStack.isEmpty() || + operatorStack.peek() !is ParsedToken.Groupable.OpenParenthesis + ) { return ParseResult.Failure( - "Encountered unexpected close parenthesis at index ${token.column} in " + - token.source + "Encountered unexpected close parenthesis at index ${token.column} in " + + token.source ) } // Discard the open parenthesis since it's be finished. @@ -117,7 +119,7 @@ class MathExpressionParser { !is ParsedToken.Groupable.Computable -> { val openParenthesis = top as ParsedToken.Groupable.OpenParenthesis return ParseResult.Failure( - "Encountered unexpected open parenthesis at index ${openParenthesis.token.column}" + "Encountered unexpected open parenthesis at index ${openParenthesis.token.column}" ) } else -> { @@ -150,7 +152,7 @@ class MathExpressionParser { val rightOperand = operandStack.pop() val leftOperand = operandStack.pop() operandStack.push( - parsedToken.parsedOperator.toMathExpression(leftOperand, rightOperand) + parsedToken.parsedOperator.toMathExpression(leftOperand, rightOperand) ) } } @@ -169,10 +171,11 @@ class MathExpressionParser { * for implied multiplication scenarios. */ private fun tokenize( - literalExpression: String, allowedVariables: List + literalExpression: String, + allowedVariables: List ): Iterable { return MathTokenizer.tokenize( - rawLiteral = literalExpression, allowedIdentifiers = allowedVariables + rawLiteral = literalExpression, allowedIdentifiers = allowedVariables ).adaptTokenStreamForImpliedMultiplication() } @@ -183,13 +186,13 @@ class MathExpressionParser { * this is implemented & the cases supported. */ private fun Iterable.adaptTokenStreamForImpliedMultiplication(): - Iterable { - val baseIterable = this - return object : Iterable { - override fun iterator(): Iterator = + Iterable { + val baseIterable = this + return object : Iterable { + override fun iterator(): Iterator = ImpliedMultiplicationIteratorAdapter(baseIterable.iterator()) + } } - } /** * Returns whether this token, as a previous token (potentially null for the first token of the @@ -217,15 +220,15 @@ class MathExpressionParser { * Returns a new [ParsedToken] for the specified token (& potentially considering the * previous token), or null if the token corresponds to an operator that isn't recognized. */ - fun parseToken(token: MathTokenizer.Token, lastToken: MathTokenizer.Token?) : ParsedToken? { + fun parseToken(token: MathTokenizer.Token, lastToken: MathTokenizer.Token?): ParsedToken? { return when (token) { is WholeNumber -> Groupable.Computable.Operand.WholeNumber(token.value) is DecimalNumber -> Groupable.Computable.Operand.DecimalNumber(token.value) is Identifier -> Groupable.Computable.Operand.Identifier(token.name) is Operator -> Groupable.Computable.Operator( - ParsedOperator.parseOperator(token, lastToken) - ?: return FailedToken.InvalidOperator(token.operator) + ParsedOperator.parseOperator(token, lastToken) + ?: return FailedToken.InvalidOperator(token.operator) ) is OpenParenthesis -> Groupable.OpenParenthesis(token) is MathTokenizer.Token.CloseParenthesis -> CloseParenthesis @@ -242,7 +245,7 @@ class MathExpressionParser { * * This class exists to simplify error-handling in the shunting-yard algorithm. */ - sealed class Groupable: ParsedToken() { + sealed class Groupable : ParsedToken() { /** * Corresponds to tokens that are computable (that is, can be converted to * [MathExpression]s). @@ -256,20 +259,20 @@ class MathExpressionParser { /** An operand that is a whole number (e.g. '2'). */ data class WholeNumber(private val value: Int) : Operand() { override fun toMathExpression(): MathExpression = - MathExpression.newBuilder() - .setConstant( - Real.newBuilder().setRational( - Fraction.newBuilder().setWholeNumber(value).setDenominator(1) - ) - ).build() + MathExpression.newBuilder() + .setConstant( + Real.newBuilder().setRational( + Fraction.newBuilder().setWholeNumber(value).setDenominator(1) + ) + ).build() } /** An operand that's a decimal (e.g. '3.14'). */ data class DecimalNumber(private val value: Double) : Operand() { override fun toMathExpression(): MathExpression = - MathExpression.newBuilder() - .setConstant(Real.newBuilder().setIrrational(value)) - .build() + MathExpression.newBuilder() + .setConstant(Real.newBuilder().setIrrational(value)) + .build() } /** @@ -278,7 +281,7 @@ class MathExpressionParser { */ data class Identifier(private val name: String) : Operand() { override fun toMathExpression(): MathExpression = - MathExpression.newBuilder().setVariable(name).build() + MathExpression.newBuilder().setVariable(name).build() } } @@ -294,10 +297,10 @@ class MathExpressionParser { } /** Corresponds to a close parenthesis token which ends a grouped expression. */ - object CloseParenthesis: ParsedToken() + object CloseParenthesis : ParsedToken() /** Corresponds to a token that represents a failure during tokenization or parsing. */ - sealed class FailedToken: ParsedToken() { + sealed class FailedToken : ParsedToken() { /** * Returns the reason the failure token was created. This is not meant to be shown to end * users, only developers. @@ -308,7 +311,7 @@ class MathExpressionParser { * Indicates an invalid operator was encountered. This typically means the tokenizer * supports operators that the parser does not. */ - data class InvalidOperator(val operator: Char): FailedToken() { + data class InvalidOperator(val operator: Char) : FailedToken() { override fun getFailureReason(): String = "Encountered unexpected operator: $operator" } @@ -316,14 +319,14 @@ class MathExpressionParser { * Indicates an identifier was encountered that doesn't correspond to any of the allowed * variables passed to the parser during parsing time. */ - data class InvalidIdentifier(val name: String): FailedToken() { + data class InvalidIdentifier(val name: String) : FailedToken() { override fun getFailureReason(): String = "Encountered invalid identifier: $name" } /** Indicates an invalid token was encountered during tokenization. */ - data class InvalidToken(private val token: MathTokenizer.Token): FailedToken() { + data class InvalidToken(private val token: MathTokenizer.Token) : FailedToken() { override fun getFailureReason(): String = - "Encountered unexpected symbol at index ${token.column} in ${token.source}" + "Encountered unexpected symbol at index ${token.column} in ${token.source}" } } } @@ -360,78 +363,78 @@ class MathExpressionParser { /** Corresponds to a binary operation (e.g. 'x + y'). */ abstract class BinaryOperator( - precedence: Int, - val associativity: Associativity, - private val protoOperator: MathBinaryOperation.Operator - ): ParsedOperator(precedence) { + precedence: Int, + val associativity: Associativity, + private val protoOperator: MathBinaryOperation.Operator + ) : ParsedOperator(precedence) { /** Returns a [MathExpression] representation of this parsed operator. */ fun toMathExpression( - leftOperand: MathExpression, - rightOperand: MathExpression + leftOperand: MathExpression, + rightOperand: MathExpression ): MathExpression = - MathExpression.newBuilder() - .setBinaryOperation( - MathBinaryOperation.newBuilder() - .setOperator(protoOperator) - .setLeftOperand(leftOperand) - .setRightOperand(rightOperand) - ).build() + MathExpression.newBuilder() + .setBinaryOperation( + MathBinaryOperation.newBuilder() + .setOperator(protoOperator) + .setLeftOperand(leftOperand) + .setRightOperand(rightOperand) + ).build() } /** Corresponds to a unary operation (e.g. '-x'). */ abstract class UnaryOperator( - precedence: Int, - private val protoOperator: MathUnaryOperation.Operator - ): ParsedOperator(precedence) { + precedence: Int, + private val protoOperator: MathUnaryOperation.Operator + ) : ParsedOperator(precedence) { /** Returns a [MathExpression] representation of this parsed operator. */ fun toMathExpression(operand: MathExpression): MathExpression = MathExpression.newBuilder() - .setUnaryOperation( - MathUnaryOperation.newBuilder() - .setOperator(protoOperator) - .setOperand(operand) - ).build() + .setUnaryOperation( + MathUnaryOperation.newBuilder() + .setOperator(protoOperator) + .setOperand(operand) + ).build() } /** Corresponds to the addition operation, e.g.: 1 + 2. */ - object Add: BinaryOperator( - precedence = 1, - associativity = Associativity.LEFT, - protoOperator = MathBinaryOperation.Operator.ADD + object Add : BinaryOperator( + precedence = 1, + associativity = Associativity.LEFT, + protoOperator = MathBinaryOperation.Operator.ADD ) /** Corresponds to the subtraction operation, e.g.: 1 - 2. */ - object Subtract: BinaryOperator( - precedence = Add.precedence, - associativity = Associativity.LEFT, - protoOperator = MathBinaryOperation.Operator.SUBTRACT + object Subtract : BinaryOperator( + precedence = Add.precedence, + associativity = Associativity.LEFT, + protoOperator = MathBinaryOperation.Operator.SUBTRACT ) /** Corresponds to the multiplication operation, e.g.: 1 * 2. */ - object Multiply: BinaryOperator( - precedence = Add.precedence + 1, - associativity = Associativity.LEFT, - protoOperator = MathBinaryOperation.Operator.MULTIPLY + object Multiply : BinaryOperator( + precedence = Add.precedence + 1, + associativity = Associativity.LEFT, + protoOperator = MathBinaryOperation.Operator.MULTIPLY ) /** Corresponds to the division operation, e.g.: 1 / 2. */ - object Divide: BinaryOperator( - precedence = Multiply.precedence, - associativity = Associativity.LEFT, - protoOperator = MathBinaryOperation.Operator.DIVIDE + object Divide : BinaryOperator( + precedence = Multiply.precedence, + associativity = Associativity.LEFT, + protoOperator = MathBinaryOperation.Operator.DIVIDE ) /** Corresponds to unary negation, e.g.: -1. */ - object Negate: UnaryOperator( - precedence = Multiply.precedence + 1, - protoOperator = MathUnaryOperation.Operator.NEGATE + object Negate : UnaryOperator( + precedence = Multiply.precedence + 1, + protoOperator = MathUnaryOperation.Operator.NEGATE ) /** Corresponds to the exponentiation operation, e.g.: 1 ^ 2. */ - object Exponentiate: BinaryOperator( - precedence = Negate.precedence + 1, - associativity = Associativity.RIGHT, - protoOperator = MathBinaryOperation.Operator.EXPONENTIATE + object Exponentiate : BinaryOperator( + precedence = Negate.precedence + 1, + associativity = Associativity.RIGHT, + protoOperator = MathBinaryOperation.Operator.EXPONENTIATE ) } @@ -440,8 +443,8 @@ class MathExpressionParser { * cases when there's implied multiplication (e.g. 2xy should be interpreted as 2*x*y). */ private class ImpliedMultiplicationIteratorAdapter( - private val baseIterator: Iterator - ): Iterator { + private val baseIterator: Iterator + ) : Iterator { private var lastToken: MathTokenizer.Token? = null private var nextToken: MathTokenizer.Token? = null @@ -453,7 +456,7 @@ class MathExpressionParser { override fun next(): MathTokenizer.Token { val (currentToken, newNextToken) = - computeCurrentTokenState(lastToken, nextToken ?: baseIterator.next()) + computeCurrentTokenState(lastToken, nextToken ?: baseIterator.next()) nextToken = newNextToken lastToken = currentToken return currentToken @@ -469,12 +472,12 @@ class MathExpressionParser { * @param nextToken the next token that should be provided to the user */ private fun computeCurrentTokenState( - lastToken: MathTokenizer.Token?, - nextToken: MathTokenizer.Token + lastToken: MathTokenizer.Token?, + nextToken: MathTokenizer.Token ): NewTokenState { return when { lastToken.impliesMultiplicationWith(nextToken) -> NewTokenState( - currentToken = synthesizeMultiplicationOperatorToken(), nextToken = nextToken + currentToken = synthesizeMultiplicationOperatorToken(), nextToken = nextToken ) else -> NewTokenState(currentToken = nextToken, nextToken = null) } @@ -514,7 +517,7 @@ class MathExpressionParser { * where multiplication should be implied. See the implementation for specifics. */ private fun MathTokenizer.Token?.impliesMultiplicationWith( - nextToken: MathTokenizer.Token + nextToken: MathTokenizer.Token ): Boolean { // Two consecutive tokens imply multiplication iff they are both variables, or one is a // variable and the other is a constant. Or, a variable/constant is followed by an open @@ -537,7 +540,8 @@ class MathExpressionParser { * if any, to cache for future calls to the iterator. */ private data class NewTokenState( - val currentToken: MathTokenizer.Token, val nextToken: MathTokenizer.Token? + val currentToken: MathTokenizer.Token, + val nextToken: MathTokenizer.Token? ) } } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt index 0cf43e850bf..053da9d8ead 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt @@ -1,8 +1,8 @@ package org.oppia.android.util.math import java.lang.IllegalStateException -import java.util.Locale import java.util.ArrayDeque +import java.util.Locale private const val DECIMAL_POINT = '.' private const val LEFT_PARENTHESIS = '(' @@ -15,8 +15,8 @@ private const val FORMAL_DIVISION_SIGN = '÷' // Only consider standard horizontal/vertical whitespace. private val VALID_WHITESPACE = listOf(' ', '\t', '\n', '\r') private val VALID_OPERATORS = listOf( - CONVENTIONAL_MULTIPLICATION_SIGN, '-', '+', CONVENTIONAL_DIVISION_SIGN, '^', - FORMAL_MULTIPLICATION_SIGN, FORMAL_DIVISION_SIGN + CONVENTIONAL_MULTIPLICATION_SIGN, '-', '+', CONVENTIONAL_DIVISION_SIGN, '^', + FORMAL_MULTIPLICATION_SIGN, FORMAL_DIVISION_SIGN ) private val VALID_DIGITS = listOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9') @@ -82,18 +82,18 @@ class MathTokenizer { /** Corresponds to an invalid identifier that was encountered. */ data class InvalidIdentifier( - override val source: String, - override val column: Int, - val name: String + override val source: String, + override val column: Int, + val name: String ) : Token() { override fun toReadableString(): String = "Invalid identifier: $name" } /** Corresponds to an invalid token that was encountered. */ data class InvalidToken( - override val source: String, - override val column: Int, - val token: String + override val source: String, + override val column: Int, + val token: String ) : Token() { override fun toReadableString(): String = "Invalid token: $token" } @@ -124,8 +124,8 @@ class MathTokenizer { * can be empty (in which case all encountered identifiers will be presumed invalid). */ fun tokenize( - rawLiteral: String, - allowedIdentifiers: List + rawLiteral: String, + allowedIdentifiers: List ): Iterable { // Verify that the provided identifiers are all valid. for (identifier in allowedIdentifiers) { @@ -143,7 +143,7 @@ class MathTokenizer { }.toSet() return object : Iterable { override fun iterator(): Iterator = - Tokenizer(lowercaseLiteral, lowercaseIdentifiers.toList()) + Tokenizer(lowercaseLiteral, lowercaseIdentifiers.toList()) } } @@ -154,8 +154,8 @@ class MathTokenizer { * Note that this class is only safe to access on a single thread. */ private class Tokenizer( - private val source: String, - private val allowedIdentifiers: List + private val source: String, + private val allowedIdentifiers: List ) : Iterator { private val singleLetterIdentifiers: List by lazy { allowedIdentifiers.filter { it.length == 1 }.map(String::first) @@ -335,7 +335,8 @@ class MathTokenizer { 1 -> listOf(parseSingleLetterIdentifier()) // Complex case: either this is one multi-letter identifier, multiple single-letter // identifiers with implied multiplication, or an invalid multi-letter identifier. - else -> parseValidMultiLetterIdentifier(nextNonIdentifierIndex) + else -> + parseValidMultiLetterIdentifier(nextNonIdentifierIndex) ?: parseMultipleSingleLetterIdentifiers(nextNonIdentifierIndex) ?: listOf(parseInvalidMultiLetterIdentifier(nextNonIdentifierIndex)) } @@ -350,7 +351,7 @@ class MathTokenizer { val potentialIdentifier = peekCharacter() skipToken() return maybeParseSingleLetterIdentifier(parsedIndex) - ?: Token.InvalidIdentifier(source, parsedIndex, potentialIdentifier.toString()) + ?: Token.InvalidIdentifier(source, parsedIndex, potentialIdentifier.toString()) } /** @@ -376,8 +377,8 @@ class MathTokenizer { private fun parseValidMultiLetterIdentifier(nextNonIdentifierIndex: Int): List? { val parsedIndex = currentIndex val potentialIdentifier = extractSubBufferString( - startIndex = currentIndex, - endIndex = nextNonIdentifierIndex + startIndex = currentIndex, + endIndex = nextNonIdentifierIndex ) return if (potentialIdentifier in multiLetterIdentifiers) { advanceIndexTo(nextNonIdentifierIndex) @@ -409,9 +410,9 @@ class MathTokenizer { val parsedIndex = currentIndex advanceIndexTo(nextNonIdentifierIndex) return Token.InvalidIdentifier( - source, - parsedIndex, - extractSubBufferString(startIndex = parsedIndex, endIndex = nextNonIdentifierIndex) + source, + parsedIndex, + extractSubBufferString(startIndex = parsedIndex, endIndex = nextNonIdentifierIndex) ) } diff --git a/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel index a4187f0dede..f9cf0e1a190 100644 --- a/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel @@ -31,7 +31,7 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ ":dagger", - "//model:test_models", + "//model/src/main/proto:test_models", "//testing", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_extensions_truth-liteproto-extension", diff --git a/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel index 43c6ea60c0c..075455e6297 100644 --- a/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel @@ -87,7 +87,7 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ ":dagger", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_extensions_truth-liteproto-extension", "//third_party:junit_junit", diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel new file mode 100644 index 00000000000..723a197f223 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -0,0 +1,46 @@ +""" +TODO: add docs +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "MathExpressionParserTest", + srcs = ["MathExpressionParserTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.MathExpressionParserTest", + test_manifest = "//utility:test_manifest", + deps = [ + ":dagger", + "//model/src/main/proto:math_java_proto_lite", + "//testing:assertion_helpers", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:parser", + ], +) + +oppia_android_test( + name = "MathTokenizerTest", + srcs = ["MathTokenizerTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.MathTokenizerTest", + test_manifest = "//utility:test_manifest", + deps = [ + ":dagger", + "//model/src/main/proto:math_java_proto_lite", + "//testing:assertion_helpers", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:tokenizer", + ], +) + +dagger_rules() diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index 70763aa6fee..9e9f29ee842 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -22,13 +22,11 @@ import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.Real import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL +import org.oppia.android.testing.assertThrows import org.oppia.android.util.math.MathExpressionParser.Companion.ParseResult import org.oppia.android.util.math.MathExpressionParser.Companion.ParseResult.Failure import org.oppia.android.util.math.MathExpressionParser.Companion.ParseResult.Success import org.robolectric.annotation.LooperMode -import kotlin.reflect.KClass -import kotlin.reflect.full.cast -import kotlin.test.fail /** Tests for [MathExpressionParser]. */ @RunWith(AndroidJUnit4::class) @@ -126,7 +124,7 @@ class MathExpressionParserTest { @Test fun testParse_longVariable_returnsExpressionWithVariable() { val result = - MathExpressionParser.parseExpression("1+lambda", allowedVariables = listOf("lambda")) + MathExpressionParser.parseExpression("1+lambda", allowedVariables = listOf("lambda")) val rootExpression = result.getExpectedSuccessfulExpression() val binOp = rootExpression.getExpectedBinaryOperation() @@ -137,7 +135,7 @@ class MathExpressionParserTest { @Test fun testParse_mixedLongAndShortVariable_returnsExpressionWithBothVariables() { val result = - MathExpressionParser.parseExpression("lambda+y", allowedVariables = listOf("y", "lambda")) + MathExpressionParser.parseExpression("lambda+y", allowedVariables = listOf("y", "lambda")) val rootExpression = result.getExpectedSuccessfulExpression() val binOp = rootExpression.getExpectedBinaryOperation() @@ -571,7 +569,7 @@ class MathExpressionParserTest { val rightMulNegatedConstant = rightMulNegateOp.operand.getExpectedRationalConstant() val leftDivConstant = divOp.leftOperand.getExpectedRationalConstant() val rightDivNegateOp = divOp.rightOperand.getExpectedUnaryOperationWithOperator(NEGATE) - val rightDivNegatedConstant = rightDivNegateOp.operand.getExpectedRationalConstant() + val rightDivNegatedConstant = rightDivNegateOp.operand.getExpectedRationalConstant() assertThat(rightMulNegatedConstant).isEqualTo(createWholeNumberFraction(2)) assertThat(leftDivConstant).isEqualTo(createWholeNumberFraction(10)) assertThat(rightDivNegatedConstant).isEqualTo(createWholeNumberFraction(1)) @@ -600,8 +598,8 @@ class MathExpressionParserTest { @Test fun testParse_complexExpression_followsPemdasWithAssociativity() { val result = MathExpressionParser.parseExpression( - "1+(13+12)/15+3*2-7/4^2^3*x+-(2*(y-3.14))", - allowedVariables = listOf("x", "y") + "1+(13+12)/15+3*2-7/4^2^3*x+-(2*(y-3.14))", + allowedVariables = listOf("x", "y") ) // Look at past tests for associativity & precedence rules that are repeated here. Expect the @@ -699,7 +697,7 @@ class MathExpressionParserTest { @Test fun testParse_constantWithLongVariable_withoutOperator_impliesMultiplication() { val result = MathExpressionParser.parseExpression( - "2lambda", allowedVariables = listOf("lambda") + "2lambda", allowedVariables = listOf("lambda") ) // Having a constant and a variable right next to each other implies multiplication. @@ -714,7 +712,7 @@ class MathExpressionParserTest { @Test fun testParse_constantWithMultipleVariables_ambiguous_withoutOperator_impliesLongVarMult() { val result = MathExpressionParser.parseExpression( - "2xyz", allowedVariables = listOf("x", "y", "z", "xyz") + "2xyz", allowedVariables = listOf("x", "y", "z", "xyz") ) // The implied multiplication here is always on long variable since it's otherwise ambiguous. @@ -729,7 +727,7 @@ class MathExpressionParserTest { @Test fun testParse_shortAndLongVariable_withoutOperator_failsToParse() { val result = MathExpressionParser.parseExpression( - "wxyz", allowedVariables = listOf("w", "xyz") + "wxyz", allowedVariables = listOf("w", "xyz") ) // There can't be implied multiplication here since 'wxyz' looks like 1 variable. Note that if @@ -778,7 +776,7 @@ class MathExpressionParserTest { @Test fun testParse_multipleShortVariables_withoutOperators_impliesMultipleMultiplications() { val result = - MathExpressionParser.parseExpression("xyz", allowedVariables = listOf("x", "y", "z")) + MathExpressionParser.parseExpression("xyz", allowedVariables = listOf("x", "y", "z")) // Having consecutive variables also implies multiplication. In this case, the expression uses // left associativity, so expect the following tree: @@ -799,7 +797,7 @@ class MathExpressionParserTest { @Test fun testParse_shortVariables_withAmbiguousLongVariable_noOperators_impliesSingleVariable() { val result = MathExpressionParser.parseExpression( - "xyz", allowedVariables = listOf("x", "y", "z", "xyz") + "xyz", allowedVariables = listOf("x", "y", "z", "xyz") ) // 'xyz' is ambiguous in this case, but a single variable should be preferred since it's an @@ -812,7 +810,7 @@ class MathExpressionParserTest { @Test fun testParse_shortVariables_withAmbiguousLongVariable_withOperator_hasMultipleVariables() { val result = MathExpressionParser.parseExpression( - "x*yz", allowedVariables = listOf("x", "y", "z", "xyz") + "x*yz", allowedVariables = listOf("x", "y", "z", "xyz") ) // Unlike the above test, the single operator is sufficient to disambiguate the the x, y, z vs. @@ -1123,7 +1121,7 @@ class MathExpressionParserTest { } private fun MathExpression.getExpectedUnaryOperationWithOperator( - operator: MathUnaryOperation.Operator + operator: MathUnaryOperation.Operator ): MathUnaryOperation { val expectedOp = getExpectedType(MathExpression::getUnaryOperation, UNARY_OPERATION) assertThat(expectedOp.operator).isEqualTo(operator) @@ -1153,18 +1151,4 @@ class MathExpressionParserTest { private fun createWholeNumberFraction(value: Int): Fraction { return Fraction.newBuilder().setWholeNumber(value).setDenominator(1).build() } - - // TODO(#89): Move to a common test library. - private fun assertThrows(type: KClass, operation: () -> Unit): T { - try { - operation() - fail("Expected to encounter exception of $type") - } catch (t: Throwable) { - if (type.isInstance(t)) { - return type.cast(t) - } - // Unexpected exception; throw it. - throw t - } - } } diff --git a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt index 62ac32ec2be..44cd48d05d2 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt @@ -4,6 +4,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith +import org.oppia.android.testing.assertThrows import org.oppia.android.util.math.MathTokenizer.Token.CloseParenthesis import org.oppia.android.util.math.MathTokenizer.Token.DecimalNumber import org.oppia.android.util.math.MathTokenizer.Token.Identifier @@ -13,9 +14,6 @@ import org.oppia.android.util.math.MathTokenizer.Token.OpenParenthesis import org.oppia.android.util.math.MathTokenizer.Token.Operator import org.oppia.android.util.math.MathTokenizer.Token.WholeNumber import org.robolectric.annotation.LooperMode -import kotlin.reflect.KClass -import kotlin.reflect.full.cast -import kotlin.test.fail /** Tests for [MathTokenizer]. */ @RunWith(AndroidJUnit4::class) @@ -493,7 +491,7 @@ class MathTokenizerTest { @Test fun testTokenize_complexExpressionWithAllTokenTypes_tokenizesEverythingInOrder() { val tokens = - MathTokenizer.tokenize("133 + 3.14 * x / (11 - 15) ^ 2 ^ 3", ALLOWED_XYZ_VARIABLES).toList() + MathTokenizer.tokenize("133 + 3.14 * x / (11 - 15) ^ 2 ^ 3", ALLOWED_XYZ_VARIABLES).toList() assertThat(tokens).hasSize(15) @@ -549,18 +547,4 @@ class MathTokenizerTest { assertThat(tokens.first()).isInstanceOf(InvalidToken::class.java) assertThat((tokens.first() as InvalidToken).token).isEqualTo("∫") } - - // TODO(#89): Move to a common test library. - private fun assertThrows(type: KClass, operation: () -> Unit): T { - try { - operation() - fail("Expected to encounter exception of $type") - } catch (t: Throwable) { - if (type.isInstance(t)) { - return type.cast(t) - } - // Unexpected exception; throw it. - throw t - } - } } From 4fdf646932711e29d0653fd746bdc4344dee4f5a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 29 Oct 2021 19:29:13 -0700 Subject: [PATCH 071/289] Add new tokenizer & parser. This introduces a new tokenizer that's based on sequences rather than a buffer. It's considerably shorter & simpler than the previous version. This also introduces a parser specific to numeric expressions. Through tests, this has been verified to have correct associativity & order of operations, except for implicit multiplication (which still needs to be fixed). Exponentiation was specifically validated for associativity, too. This introduces a new model spec for MatchesUpToTrivialManipulations (though more work is probably needed here). This PR also introduces a custom Truth subject for testing the new tokenizer and a bunch of test cases. MANY more test cases are yet to be added, but this was used to verify the tokenizer at least at a high level. This PR also introduces custom Truth subjects & a DSL for verifying the constructed AST from numeric expressions. This is much easier to read & understand as compared with previous expression tests. ~29 test cases were added to verify various parts of the parser, but it's expected dozens more test cases will be added to thoroughly test the parser. A basic evaluation system was added (with integer being a new Real value, and various efforts to try and retain perfect precision during evaluation). This was added to various test cases for the parser to verify more concretely that the order of operations is correct per the ordering of the AST. Finally, the implementation & tests prove that the grammar is properly LL(1). Future iterations could optimize parsing to be table-based if performance ends up being a problem (though I doubt parsing will be the bottleneck here since evaluation seems far more expensive). --- model/src/main/proto/math.proto | 56 +- .../org/oppia/android/util/math/BUILD.bazel | 26 + .../android/util/math/FractionExtensions.kt | 37 +- .../util/math/MathExpressionExtensions.kt | 305 ++++- .../oppia/android/util/math/MathTokenizer2.kt | 130 ++ .../util/math/NumericExpressionParser.kt | 353 ++++++ .../android/util/math/PeekableIterator.kt | 38 + .../org/oppia/android/util/math/BUILD.bazel | 22 +- .../android/util/math/MathTokenizerTest.kt | 189 ++- .../util/math/NumericExpressionParserTest.kt | 1096 +++++++++++++++++ 10 files changed, 2189 insertions(+), 63 deletions(-) create mode 100644 utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 5da8a15b477..3451ad3a48a 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -21,6 +21,7 @@ message MathExpression { string variable = 2; MathBinaryOperation binary_operation = 3; MathUnaryOperation unary_operation = 4; + MathFunctionCall function_call = 5; } } @@ -49,19 +50,72 @@ message MathUnaryOperation { OPERATOR_UNSPECIFIED = 0; // Represents negating a value, e.g.: -y. NEGATE = 1; + // Represents indicating a value as positive, e.g.: +y. + POSITIVE = 2; } Operator operator = 1; MathExpression operand = 2; } +message MathFunctionCall { + enum FunctionType { + FUNCTION_UNSPECIFIED = 0; + SQUARE_ROOT = 1; + } + + FunctionType function_type = 1; + MathExpression argument = 2; +} + message Real { oneof real_type { Fraction rational = 1; // Represents a decimal value. Technically these can sometimes be rational, but given IEEE-754 // rounding errors we need to treat these values as irrational and non-factorable. double irrational = 2; + int32 integer = 3; + } +} + +message ComparableOperationList { + message ComparableOperation { + oneof comparison_type { + CommutativeAccumulation commutative_accumulation = 1; + NonCommutativeOperation non_commutative_operation = 2; + Real constant_term = 3; + string variable_term = 4; + } + } + message CommutativeAccumulation { + enum AccumulationType { + ACCUMULATION_TYPE_UNSPECIFIED = 0; + SUMMATION = 1; + PRODUCT = 2; + } + + AccumulationType accumulation_type = 1; + repeated ComparableOperation combined_operations = 2; } + message NonCommutativeOperation { + bool is_negated = 1; + + oneof operation_type { + BinaryOperation division = 2; + BinaryOperation exponentiation = 3; + UnaryOperation square_root = 4; + } + + message BinaryOperation { + ComparableOperation left_operand = 1; + ComparableOperation right_operand = 2; + } + message UnaryOperation { + ComparableOperation operand = 1; + } + } + + ComparableOperation root_operation = 1; } message Polynomial { @@ -69,7 +123,7 @@ message Polynomial { message Term { Real coefficient = 1; - repeated Variable variable = 2; + repeated Variable variable = 2; message Variable { string name = 1; diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index db12f3ec1f6..e4c1c1fab8c 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -33,12 +33,38 @@ kt_android_library( ], ) +kt_android_library( + name = "numeric_expression_parser", + srcs = [ + "NumericExpressionParser.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":peekable_iterator", + ":tokenizer", + "//model/src/main/proto:math_java_proto_lite", + ], +) + kt_android_library( name = "tokenizer", srcs = [ "MathTokenizer.kt", + "MathTokenizer2.kt", ], visibility = [ "//:oppia_testing_visibility", ], + deps = [ + ":peekable_iterator", + ], +) + +kt_android_library( + name = "peekable_iterator", + srcs = [ + "PeekableIterator.kt", + ], ) diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index cde2eb58bca..b1834e6c081 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -1,6 +1,7 @@ package org.oppia.android.util.math import org.oppia.android.app.model.Fraction +import kotlin.math.abs import kotlin.math.absoluteValue /** @@ -46,22 +47,25 @@ fun Fraction.isOnlyWholeNumber(): Boolean { } /** - * Returns a float version of this fraction. + * Returns this fraction as a whole number. Note that this will not return a value that is + * mathematically equivalent to this fraction unless [isOnlyWholeNumber] returns true. + */ +fun Fraction.toWholeNumber(): Int = if (isNegative) -wholeNumber else wholeNumber + +/** [Float] version of [toDouble]. */ +fun Fraction.toFloat(): Float = toDouble().toFloat() + +/** + * Returns a [Double] version of this fraction. * * See: https://github.com/oppia/oppia/blob/37285a/core/templates/dev/head/domain/objects/FractionObjectFactory.ts#L73. */ -fun Fraction.toFloat(): Float { - val totalParts = ((wholeNumber * denominator) + numerator).toFloat() - val floatVal = totalParts / denominator.toFloat() +fun Fraction.toDouble(): Double { + val totalParts = ((wholeNumber.toDouble() * denominator.toDouble()) + numerator.toDouble()) + val floatVal = totalParts / denominator.toDouble() return if (isNegative) -floatVal else floatVal } -/** - * Double version of [toFloat] (note that this doesn't actually guarantee additional precision over - * toFloat(). - */ -fun Fraction.toDouble(): Double = toFloat().toDouble() - /** * Returns this fraction in its most simplified form. * @@ -171,7 +175,7 @@ operator fun Fraction.div(rhs: Fraction): Fraction { } /** Returns the inverse improper fraction representation of this fraction. */ -private fun Fraction.toInvertedImproperForm(): Fraction { +fun Fraction.toInvertedImproperForm(): Fraction { val improper = toImproperForm() return improper.toBuilder() .setNumerator(improper.denominator) @@ -184,6 +188,17 @@ operator fun Fraction.unaryMinus(): Fraction { return toBuilder().setIsNegative(!isNegative).build() } +/** Returns the [Fraction] representation of this integer (as a whole number fraction). */ +fun Int.toWholeNumberFraction(): Fraction { + val intValue = this + return Fraction.newBuilder().apply { + isNegative = intValue < 0 + wholeNumber = abs(intValue) + numerator = 0 + denominator = 1 + }.build() +} + /** Returns the greatest common divisor between two integers. */ fun gcd(x: Int, y: Int): Int { return if (y == 0) x else gcd(y, x % y) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 2418e97551c..599d587d3a2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -5,14 +5,23 @@ import org.oppia.android.app.model.MathBinaryOperation import org.oppia.android.app.model.MathExpression import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Polynomial.Term import org.oppia.android.app.model.Polynomial.Term.Variable import org.oppia.android.app.model.Real +import org.oppia.android.app.model.Real.RealTypeCase.INTEGER +import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET +import kotlin.math.abs import kotlin.math.pow +import kotlin.math.sqrt // TODO: split up this extensions file into multiple, clean it up, reorganize, and add tests. @@ -21,10 +30,63 @@ import kotlin.math.pow // x^(2^3) and ((x^2+2x+1)/(x+1)) won't be considered polynomials, but it can be seen that they are // indeed after being simplified. This implementation doesn't yet support recognizing binomials // (e.g. (x+1)^2) but needs to be updated to. The representation for Polynomial doesn't quite -// support much outside of general form. +// support much outside of general form (except negative exponents). -// Consider expressions like x/2 (should be treated like (1/2)x). -// Consider: (1+2)*x or (1+2)x +// TODO: Consider expressions like x/2 (should be treated like (1/2)x). +// TODO: Consider: (1+2)*x or (1+2)x +// TODO: Make sure that all 'when' cases here do not use 'else' branches to ensure structural +// changes require changing logic. + +// TODO: add proper error channels for the return value. +fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() + +fun MathExpression.evaluate(): Real? { + return when (expressionTypeCase) { + CONSTANT -> constant + VARIABLE -> null // Variables not supported in numeric expressions. + BINARY_OPERATION -> binaryOperation.evaluate() + UNARY_OPERATION -> unaryOperation.evaluate() + FUNCTION_CALL -> functionCall.evaluate() + EXPRESSIONTYPE_NOT_SET, null -> null + } +} + +private fun MathBinaryOperation.evaluate(): Real? { + return when (operator) { + MathBinaryOperation.Operator.ADD -> + rightOperand.evaluate()?.let { leftOperand.evaluate()?.plus(it) } + MathBinaryOperation.Operator.SUBTRACT -> + rightOperand.evaluate()?.let { leftOperand.evaluate()?.minus(it) } + MathBinaryOperation.Operator.MULTIPLY -> + rightOperand.evaluate()?.let { leftOperand.evaluate()?.times(it) } + MathBinaryOperation.Operator.DIVIDE -> + rightOperand.evaluate()?.let { leftOperand.evaluate()?.div(it) } + MathBinaryOperation.Operator.EXPONENTIATE -> + rightOperand.evaluate()?.let { leftOperand.evaluate()?.pow(it) } + MathBinaryOperation.Operator.OPERATOR_UNSPECIFIED, + MathBinaryOperation.Operator.UNRECOGNIZED, + null -> null + } +} + +private fun MathUnaryOperation.evaluate(): Real? { + return when (operator) { + MathUnaryOperation.Operator.NEGATE -> operand.evaluate()?.let { -it } + MathUnaryOperation.Operator.POSITIVE -> operand.evaluate() // '+2' is the same as just '2'. + MathUnaryOperation.Operator.OPERATOR_UNSPECIFIED, + MathUnaryOperation.Operator.UNRECOGNIZED, + null -> null + } +} + +private fun MathFunctionCall.evaluate(): Real? { + return when (functionType) { + MathFunctionCall.FunctionType.SQUARE_ROOT -> argument.evaluate()?.let { sqrt(it) } + MathFunctionCall.FunctionType.FUNCTION_UNSPECIFIED, + MathFunctionCall.FunctionType.UNRECOGNIZED, + null -> null + } +} // Also: consider how to perform expression analysis for non-polynomial trees @@ -424,11 +486,16 @@ private fun Real.isApproximatelyEqualTo(value: Double): Boolean { private fun Real.isApproximatelyZero(): Boolean = isApproximatelyEqualTo(0.0) private fun Real.toDouble(): Double { - return if (hasRational()) rational.toDouble() else irrational + return when (realTypeCase) { + RATIONAL -> rational.toDouble() + INTEGER -> integer.toDouble() + IRRATIONAL -> irrational + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + } } private fun Real.recompute(transform: (Real.Builder) -> Real.Builder): Real { - return transform(toBuilder().clearRational().clearIrrational()).build() + return transform(toBuilder().clearRational().clearIrrational().clearInteger()).build() } private fun combine( @@ -436,47 +503,71 @@ private fun combine( rhs: Real, leftRationalRightRationalOp: (Fraction, Fraction) -> Fraction, leftRationalRightIrrationalOp: (Fraction, Double) -> Double, + leftRationalRightIntegerOp: (Fraction, Int) -> Fraction, leftIrrationalRightRationalOp: (Double, Fraction) -> Double, - leftIrrationalRightIrrationalOp: (Double, Double) -> Double + leftIrrationalRightIrrationalOp: (Double, Double) -> Double, + leftIrrationalRightIntegerOp: (Double, Int) -> Double, + leftIntegerRightRationalOp: (Int, Fraction) -> Fraction, + leftIntegerRightIrrationalOp: (Int, Double) -> Double, + leftIntegerRightIntegerOp: (Int, Int) -> Real, ): Real { return when (lhs.realTypeCase) { - Real.RealTypeCase.RATIONAL -> { + RATIONAL -> { // Left-hand side is Fraction. when (rhs.realTypeCase) { - Real.RealTypeCase.RATIONAL -> + RATIONAL -> lhs.recompute { it.setRational(leftRationalRightRationalOp(lhs.rational, rhs.rational)) } - Real.RealTypeCase.IRRATIONAL -> + IRRATIONAL -> lhs.recompute { it.setIrrational(leftRationalRightIrrationalOp(lhs.rational, rhs.irrational)) } - else -> throw Exception("Invalid real: $rhs.") + INTEGER -> + lhs.recompute { it.setRational(leftRationalRightIntegerOp(lhs.rational, rhs.integer)) } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") } } - Real.RealTypeCase.IRRATIONAL -> { + IRRATIONAL -> { // Left-hand side is a double. when (rhs.realTypeCase) { - Real.RealTypeCase.RATIONAL -> + RATIONAL -> lhs.recompute { it.setIrrational(leftIrrationalRightRationalOp(lhs.irrational, rhs.rational)) } - Real.RealTypeCase.IRRATIONAL -> + IRRATIONAL -> lhs.recompute { it.setIrrational(leftIrrationalRightIrrationalOp(lhs.irrational, rhs.irrational)) } - else -> throw Exception("Invalid real: $rhs.") + INTEGER -> + lhs.recompute { + it.setIrrational(leftIrrationalRightIntegerOp(lhs.irrational, rhs.integer)) + } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") } } - else -> throw Exception("Invalid real: $lhs.") + INTEGER -> { + // Left-hand side is an integer. + when (rhs.realTypeCase) { + RATIONAL -> + lhs.recompute { it.setRational(leftIntegerRightRationalOp(lhs.integer, rhs.rational)) } + IRRATIONAL -> + lhs.recompute { + it.setIrrational(leftIntegerRightIrrationalOp(lhs.integer, rhs.irrational)) + } + INTEGER -> leftIntegerRightIntegerOp(lhs.integer, rhs.integer) + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $lhs.") } } private fun Real.pow(rhs: Real): Real { // Powers can really only be effectively done via floats or whole-number only fractions. return when (realTypeCase) { - Real.RealTypeCase.RATIONAL -> { + RATIONAL -> { // Left-hand side is Fraction. when (rhs.realTypeCase) { - Real.RealTypeCase.RATIONAL -> recompute { + RATIONAL -> recompute { if (rhs.rational.isOnlyWholeNumber()) { // The fraction can be retained. it.setRational(rational.pow(rhs.rational.wholeNumber)) @@ -486,62 +577,202 @@ private fun Real.pow(rhs: Real): Real { it.setIrrational(rational.toDouble().pow(rhs.rational.toDouble())) } } - Real.RealTypeCase.IRRATIONAL -> recompute { it.setIrrational(rational.pow(rhs.irrational)) } - else -> throw Exception("Invalid real: $rhs.") + IRRATIONAL -> recompute { it.setIrrational(rational.pow(rhs.irrational)) } + INTEGER -> recompute { it.setRational(rational.pow(rhs.integer)) } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") } } - Real.RealTypeCase.IRRATIONAL -> { + IRRATIONAL -> { // Left-hand side is a double. when (rhs.realTypeCase) { - Real.RealTypeCase.RATIONAL -> recompute { it.setIrrational(irrational.pow(rhs.rational)) } - Real.RealTypeCase.IRRATIONAL -> - recompute { it.setIrrational(irrational.pow(rhs.irrational)) } - else -> throw Exception("Invalid real: $rhs.") + RATIONAL -> recompute { it.setIrrational(irrational.pow(rhs.rational)) } + IRRATIONAL -> recompute { it.setIrrational(irrational.pow(rhs.irrational)) } + INTEGER -> recompute { it.setIrrational(irrational.pow(rhs.integer)) } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + INTEGER -> { + // Left-hand side is an integer. + when (rhs.realTypeCase) { + RATIONAL -> { + if (rhs.rational.isOnlyWholeNumber()) { + // Whole number-only fractions are effectively just int^int. + integer.pow(rhs.rational.wholeNumber) + } else { + // Otherwise, raising by a fraction will result in an irrational number. + recompute { it.setIrrational(integer.toDouble().pow(rhs.rational.toDouble())) } + } + } + IRRATIONAL -> recompute { it.setIrrational(integer.toDouble().pow(rhs.irrational)) } + INTEGER -> integer.pow(rhs.integer) + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") } } - else -> throw Exception("Invalid real: $this.") + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") } } private operator fun Real.unaryMinus(): Real { return when (realTypeCase) { - Real.RealTypeCase.RATIONAL -> recompute { it.setRational(-rational) } - Real.RealTypeCase.IRRATIONAL -> recompute { it.setIrrational(-irrational) } - else -> throw Exception("Invalid real: $this.") + RATIONAL -> recompute { it.setRational(-rational) } + IRRATIONAL -> recompute { it.setIrrational(-irrational) } + INTEGER -> recompute { it.setInteger(-integer) } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") } } private operator fun Real.plus(rhs: Real): Real { - return combine(this, rhs, Fraction::plus, Fraction::plus, Double::plus, Double::plus) + return combine( + this, rhs, Fraction::plus, Fraction::plus, Fraction::plus, Double::plus, Double::plus, + Double::plus, Int::plus, Int::plus, Int::add + ) } private operator fun Real.minus(rhs: Real): Real { - return combine(this, rhs, Fraction::minus, Fraction::minus, Double::minus, Double::minus) + return combine( + this, rhs, Fraction::minus, Fraction::minus, Fraction::minus, Double::minus, Double::minus, + Double::minus, Int::minus, Int::minus, Int::subtract + ) } private operator fun Real.times(rhs: Real): Real { - return combine(this, rhs, Fraction::times, Fraction::times, Double::times, Double::times) + return combine( + this, rhs, Fraction::times, Fraction::times, Fraction::times, Double::times, Double::times, + Double::times, Int::times, Int::times, Int::multiply + ) } private operator fun Real.div(rhs: Real): Real { - return combine(this, rhs, Fraction::div, Fraction::div, Double::div, Double::div) + return combine( + this, rhs, Fraction::div, Fraction::div, Fraction::div, Double::div, Double::div, Double::div, + Int::div, Int::div, Int::divide + ) +} + +private fun sqrt(real: Real): Real { + return when (real.realTypeCase) { + RATIONAL -> sqrt(real.rational) + IRRATIONAL -> real.recompute { it.setIrrational(sqrt(real.irrational)) } + INTEGER -> sqrt(real.integer) + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $real.") + } } +private fun Real.isInteger(): Boolean = realTypeCase == INTEGER + private fun Double.pow(rhs: Fraction): Double = this.pow(rhs.toDouble()) private fun Fraction.pow(rhs: Double): Double = toDouble().pow(rhs) private operator fun Double.plus(rhs: Fraction): Double = this + rhs.toFloat() private operator fun Fraction.plus(rhs: Double): Double = toFloat() + rhs +private operator fun Fraction.plus(rhs: Int): Fraction = this + rhs.toWholeNumberFraction() +private operator fun Int.plus(rhs: Fraction): Fraction = toWholeNumberFraction() + rhs private operator fun Double.minus(rhs: Fraction): Double = this - rhs.toFloat() private operator fun Fraction.minus(rhs: Double): Double = toFloat() - rhs +private operator fun Fraction.minus(rhs: Int): Fraction = this - rhs.toWholeNumberFraction() +private operator fun Int.minus(rhs: Fraction): Fraction = toWholeNumberFraction() - rhs private operator fun Double.times(rhs: Fraction): Double = this * rhs.toFloat() private operator fun Fraction.times(rhs: Double): Double = toFloat() * rhs +private operator fun Fraction.times(rhs: Int): Fraction = this * rhs.toWholeNumberFraction() +private operator fun Int.times(rhs: Fraction): Fraction = toWholeNumberFraction() * rhs private operator fun Double.div(rhs: Fraction): Double = this / rhs.toFloat() private operator fun Fraction.div(rhs: Double): Double = toFloat() / rhs +private operator fun Fraction.div(rhs: Int): Fraction = this / rhs.toWholeNumberFraction() +private operator fun Int.div(rhs: Fraction): Fraction = toWholeNumberFraction() / rhs + +private fun Int.add(rhs: Int): Real = Real.newBuilder().apply { integer = this@add + rhs }.build() +private fun Int.subtract(rhs: Int): Real = Real.newBuilder().apply { + integer = this@subtract - rhs +}.build() +private fun Int.multiply(rhs: Int): Real = Real.newBuilder().apply { + integer = this@multiply * rhs +}.build() +private fun Int.divide(rhs: Int): Real = Real.newBuilder().apply { + // If rhs divides this integer, retain the integer. + val lhs = this@divide + if ((lhs % rhs) == 0) { + integer = lhs / rhs + } else { + // Otherwise, keep precision by turning the division into a fraction. + rational = Fraction.newBuilder().apply { + isNegative = (lhs < 0) xor (rhs < 0) + numerator = abs(lhs) + denominator = abs(rhs) + }.build() + } +}.build() private fun Fraction.pow(exp: Int): Fraction { - if (exp == 0) return Fraction.newBuilder().setWholeNumber(1).setDenominator(1).build() - if (exp == 1) return this - var newValue = this - for (i in 1 until exp) newValue *= this - return newValue + return when { + exp == 0 -> Fraction.newBuilder().setWholeNumber(1).setDenominator(1).build() + exp == 1 -> this + // x^-2 == 1/(x^2). + exp < 1 -> pow(-exp).toInvertedImproperForm().toProperForm() + else -> { // i > 1 + var newValue = this + for (i in 1 until exp) newValue *= this + return newValue + } + } +} + +private fun sqrt(fraction: Fraction): Real { + val improper = fraction.toImproperForm() + + // Attempt to take the root of the fraction's numerator & denominator. + val numeratorRoot = sqrt(improper.numerator) + val denominatorRoot = sqrt(improper.denominator) + + // If both values stayed as integers, the original fraction can be retained. Otherwise, the + // fraction must be evaluated by performing a division. + return Real.newBuilder().apply { + if (numeratorRoot.realTypeCase == denominatorRoot.realTypeCase && numeratorRoot.isInteger()) { + val rootedFraction = Fraction.newBuilder().apply { + isNegative = improper.isNegative + numerator = numeratorRoot.integer + denominator = denominatorRoot.integer + }.build().toProperForm() + if (rootedFraction.isOnlyWholeNumber()) { + // If the fractional form doesn't need to be kept, remove it. + integer = rootedFraction.toWholeNumber() + } else { + rational = rootedFraction + } + } else { + irrational = numeratorRoot.toDouble() + } + }.build() +} + +private fun Int.pow(exp: Int): Real { + return when { + exp == 0 -> Real.newBuilder().apply { integer = 0 }.build() + exp == 1 -> Real.newBuilder().apply { integer = this@pow }.build() + exp < 0 -> Real.newBuilder().apply { rational = toWholeNumberFraction().pow(exp) }.build() + else -> { + // exp > 1 + var computed = this + for (i in 0 until exp - 1) computed *= this + Real.newBuilder().apply { integer = computed }.build() + } + } +} + +private fun sqrt(int: Int): Real { + // First, check if the integer is a square. Reference for possible methods: + // https://www.researchgate.net/post/How-to-decide-if-a-given-number-will-have-integer-square-root-or-not. + var potentialRoot = 2 + while ((potentialRoot * potentialRoot) < int) { + potentialRoot++ + } + if (potentialRoot * potentialRoot == int) { + // There's an exact integer representation of the root. + return Real.newBuilder().apply { + integer = potentialRoot + }.build() + } + + // Otherwise, compute the irrational square root. + return Real.newBuilder().apply { + irrational = sqrt(int.toDouble()) + }.build() } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt new file mode 100644 index 00000000000..8ce54ec55d0 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt @@ -0,0 +1,130 @@ +package org.oppia.android.util.math + +import java.lang.StringBuilder + +// TODO: rename to MathTokenizer & add documentation. +// TODO: consider a more efficient implementation that uses 1 underlying buffer (which could still +// be sequence-backed) with a forced lookahead-of-1 API, to also avoid rebuffering when parsing +// sequences of characters like for integers. + +// TODO: add customization to omit certain symbols (such as variables for numeric expressions?) +class MathTokenizer2 private constructor() { + companion object { + fun tokenize(input: String): Sequence = tokenize(input.toCharArray().asSequence()) + + fun tokenize(input: Sequence): Sequence { + val chars = PeekableIterator.fromSequence(input) + return generateSequence { + // Consume any whitespace that might precede a valid token. + chars.consumeWhitespace() + + // Parse the next token from the underlying sequence. + when (chars.peek()) { + in '0'..'9' -> tokenizeIntegerOrRealNumber(chars) + in 'a'..'z', in 'A'..'Z' -> tokenizeVariableOrFunctionName(chars) + '√' -> tokenizeSymbol(chars) { Token.SquareRootSymbol } + '+' -> tokenizeSymbol(chars) { Token.PlusSymbol } + // TODO: add tests for different subtraction/minus symbols. + '-', '−' -> tokenizeSymbol(chars) { Token.MinusSymbol } + '*', '×' -> tokenizeSymbol(chars) { Token.MultiplySymbol } + '/', '÷' -> tokenizeSymbol(chars) { Token.DivideSymbol } + '^' -> tokenizeSymbol(chars) { Token.ExponentiationSymbol } + '=' -> tokenizeSymbol(chars) { Token.EqualsSymbol } + '(' -> tokenizeSymbol(chars) { Token.LeftParenthesisSymbol } + ')' -> tokenizeSymbol(chars) { Token.RightParenthesisSymbol } + null -> null // End of stream. + else -> { // Invalid character. + chars.next() // Parse the invalid character. + Token.InvalidToken + } + } + } + } + + private fun tokenizeIntegerOrRealNumber(chars: PeekableIterator): Token { + val integerPart1 = parseInteger(chars) ?: return Token.InvalidToken + chars.consumeWhitespace() // Whitespace is allowed between digits and the '.'. + return if (chars.peek() == '.') { + chars.next() // Parse the "." since it will be re-added later. + chars.consumeWhitespace() // Whitespace is allowed between the '.' and following digits. + + // Another integer must follow the ".". + val integerPart2 = parseInteger(chars) ?: return Token.InvalidToken + + val doubleValue = "$integerPart1.$integerPart2".toDoubleOrNull() + ?: return Token.InvalidToken + Token.PositiveRealNumber(doubleValue) + } else { + Token.PositiveInteger(integerPart1.toIntOrNull() ?: return Token.InvalidToken) + } + } + + private fun tokenizeVariableOrFunctionName(chars: PeekableIterator): Token { + val firstChar = chars.next() + val nextChar = chars.peek() + return if (firstChar == 's' && nextChar == 'q') { + // With 'sq' next to each other, 'rt' is expected to follow. + chars.expectNextValue { 'q' } ?: return Token.InvalidToken + chars.expectNextValue { 'r' } ?: return Token.InvalidToken + chars.expectNextValue { 't' } ?: return Token.InvalidToken + Token.FunctionName("sqrt") + } else Token.VariableName(firstChar.toString()) + } + + private fun tokenizeSymbol(chars: PeekableIterator, factory: () -> Token): Token { + chars.next() // Parse the symbol. + return factory() + } + + private fun parseInteger(chars: PeekableIterator): String? { + val integerBuilder = StringBuilder() + while (chars.peek() in '0'..'9') { + integerBuilder.append(chars.next()) + } + return if (integerBuilder.isNotEmpty()) { + integerBuilder.toString() + } else null // Failed to parse; no digits. + } + + sealed class Token { + class PositiveInteger(val parsedValue: Int) : Token() + + class PositiveRealNumber(val parsedValue: Double) : Token() + + class VariableName(val parsedName: String) : Token() + + class FunctionName(val parsedName: String) : Token() + + object MinusSymbol : Token() + + object SquareRootSymbol : Token() + + object PlusSymbol : Token() + + object MultiplySymbol : Token() + + object DivideSymbol : Token() + + object ExponentiationSymbol : Token() + + object EqualsSymbol : Token() + + object LeftParenthesisSymbol : Token() + + object RightParenthesisSymbol : Token() + + // TODO: add context to line & index, and enum for context on failure. + object InvalidToken : Token() + } + + // TODO: consider whether to use the more correct & expensive Java Char.isWhitespace(). + private fun Char.isWhitespace(): Boolean = when (this) { + ' ', '\t', '\n', '\r' -> true + else -> false + } + + private fun PeekableIterator.consumeWhitespace() { + while (peek()?.isWhitespace() == true) next() + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt new file mode 100644 index 00000000000..37e58953ac2 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt @@ -0,0 +1,353 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.Real +import org.oppia.android.util.math.MathTokenizer2.Companion.Token +import org.oppia.android.util.math.MathTokenizer2.Companion.Token.DivideSymbol +import org.oppia.android.util.math.MathTokenizer2.Companion.Token.ExponentiationSymbol +import org.oppia.android.util.math.MathTokenizer2.Companion.Token.FunctionName +import org.oppia.android.util.math.MathTokenizer2.Companion.Token.LeftParenthesisSymbol +import org.oppia.android.util.math.MathTokenizer2.Companion.Token.MinusSymbol +import org.oppia.android.util.math.MathTokenizer2.Companion.Token.MultiplySymbol +import org.oppia.android.util.math.MathTokenizer2.Companion.Token.PlusSymbol +import org.oppia.android.util.math.MathTokenizer2.Companion.Token.PositiveInteger +import org.oppia.android.util.math.MathTokenizer2.Companion.Token.PositiveRealNumber +import org.oppia.android.util.math.MathTokenizer2.Companion.Token.RightParenthesisSymbol +import org.oppia.android.util.math.MathTokenizer2.Companion.Token.SquareRootSymbol + +class NumericExpressionParser(private val rawExpression: String) { + private val tokens: PeekableIterator by lazy { + PeekableIterator.fromSequence(MathTokenizer2.tokenize(rawExpression)) + } + + fun parse(): MathExpression { + return parseNumericExpression().also { + // Make sure all tokens were consumed (otherwise there are trailing tokens which invalidate the + // whole expression). + if (tokens.hasNext()) throw ParseException() + } + } + + private fun parseNumericExpression(): MathExpression { + // numeric_expression = numeric_add_sub_expression ; + return parseNumericAddSubExpression() + } + + // TODO: consider consolidating this with other binary parsing to reduce the overall parser. + private fun parseNumericAddSubExpression(): MathExpression { + // numeric_add_sub_expression = + // numeric_mult_div_expression , { numeric_add_sub_expression_rhs } ; + var lastLhs = parseNumericMultDivExpression() + while (hasNextNumericAddSubExpressionRhs()) { + // numeric_add_sub_expression_rhs = + // numeric_add_expression_rhs | numeric_sub_expression_rhs ; + val (operator, rhs) = when (tokens.peek()) { + is PlusSymbol -> MathBinaryOperation.Operator.ADD to parseNumericAddExpressionRhs() + is MinusSymbol -> MathBinaryOperation.Operator.SUBTRACT to parseNumericSubExpressionRhs() + else -> throw ParseException() + } + + // Compute the next LHS if there is further addition/subtraction. + lastLhs = MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.newBuilder().apply { + this.operator = operator + leftOperand = lastLhs + rightOperand = rhs + }.build() + }.build() + } + return lastLhs + } + + private fun hasNextNumericAddSubExpressionRhs() = when (tokens.peek()) { + is PlusSymbol, is MinusSymbol -> true + else -> false + } + + private fun parseNumericAddExpressionRhs(): MathExpression { + // numeric_add_expression_rhs = plus_operator , numeric_mult_div_expression ; + consumeTokenOfType { PlusSymbol } + return parseNumericMultDivExpression() + } + + private fun parseNumericSubExpressionRhs(): MathExpression { + // numeric_sub_expression_rhs = minus_operator , numeric_mult_div_expression ; + consumeTokenOfType { MinusSymbol } + return parseNumericMultDivExpression() + } + + private fun parseNumericMultDivExpression(): MathExpression { + // numeric_mult_div_expression = + // numeric_exp_expression , { numeric_mult_div_expression_rhs } ; + var lastLhs = parseNumericExpExpression() + while (hasNextNumericMultDivExpressionRhs()) { + // numeric_mult_div_expression_rhs = + // numeric_mult_expression_rhs | numeric_div_expression_rhs ; + val (operator, rhs) = when (tokens.peek()) { + is MultiplySymbol -> + MathBinaryOperation.Operator.MULTIPLY to parseNumericMultExpressionRhs() + is DivideSymbol -> MathBinaryOperation.Operator.DIVIDE to parseNumericDivExpressionRhs() + else -> throw ParseException() + } + + // Compute the next LHS if there is further multiplication/division. + lastLhs = MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.newBuilder().apply { + this.operator = operator + leftOperand = lastLhs + rightOperand = rhs + }.build() + }.build() + } + return lastLhs + } + + private fun hasNextNumericMultDivExpressionRhs() = when (tokens.peek()) { + is MultiplySymbol, is DivideSymbol -> true + else -> false + } + + private fun parseNumericMultExpressionRhs(): MathExpression { + // numeric_mult_expression_rhs = + // multiplication_operator , numeric_exp_expression ; + consumeTokenOfType { MultiplySymbol } + return parseNumericExpExpression() + } + + private fun parseNumericDivExpressionRhs(): MathExpression { + // numeric_div_expression_rhs = division_operator , numeric_exp_expression ; + consumeTokenOfType { DivideSymbol } + return parseNumericExpExpression() + } + + private fun parseNumericExpExpression(): MathExpression { + // numeric_exp_expression = numeric_term , [ numeric_exp_expression_tail ] ; + val possibleLhs = parseNumericTerm() + return if (tokens.peek() is ExponentiationSymbol) { + parseNumericExpExpressionTail(possibleLhs) + } else possibleLhs + } + + // Use tail recursion so that the last exponentiation is evaluated first, and right-to-left + // associativity can be kept via backtracking. + private fun parseNumericExpExpressionTail(lhs: MathExpression): MathExpression { + // numeric_exp_expression_tail = + // exponentiation_operator , numeric_exp_expression ; + consumeTokenOfType { ExponentiationSymbol } + return MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = MathBinaryOperation.Operator.EXPONENTIATE + leftOperand = lhs + rightOperand = parseNumericExpExpression() + }.build() + }.build() + } + + private fun parseNumericTerm(): MathExpression { + // numeric_term = + // implicitly_multipliable_with_number_term | numeric_plus_minus_unary_term ; + return if (hasNextImplicitlyMultipliableWithNumberTerm()) { + parseImplicitlyMultipliableWithNumberTerm() + } else parseNumericPlusMinusUnaryTerm() + } + + private fun parseImplicitlyMultipliableWithNumberTerm(): MathExpression { + // implicitly_multipliable_with_number_term = + // number_with_implicit_multiplication + // | implicitly_multipliable_without_number_term ; + return when (tokens.peek()) { + is PositiveInteger, is PositiveRealNumber -> parseNumberWithImplicitMultiplication() + else -> parseImplicitlyMultipliableWithoutNumberTerm() + } + } + + private fun hasNextImplicitlyMultipliableWithNumberTerm(): Boolean = when (tokens.peek()) { + is PositiveInteger, is PositiveRealNumber, is FunctionName, is LeftParenthesisSymbol, + is SquareRootSymbol -> true + else -> false + } + + private fun parseNumberWithImplicitMultiplication(): MathExpression { + // number_with_implicit_multiplication = + // number , [ implicitly_multipliable_without_number_term ] ; + // TODO: add specification for implicit multiplication (for reconstruction). + val realOperand = parseNumber() + return if (hasNextImplicitlyMultipliableWithoutNumberTerm()) { + MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = MathBinaryOperation.Operator.MULTIPLY + leftOperand = realOperand + rightOperand = parseImplicitlyMultipliableWithoutNumberTerm() + }.build() + }.build() + } else realOperand + } + + private fun parseImplicitlyMultipliableWithoutNumberTerm(): MathExpression { + // implicitly_multipliable_without_number_term = + // numeric_function_expression_with_implicit_multiplication + // | numeric_group_expression_with_implicit_multiplication + // | numeric_rooted_term_with_implicit_multiplication ; + return when (tokens.peek()) { + is FunctionName -> parseNumericFunctionExpressionWithImplicitMultiplication() + is LeftParenthesisSymbol -> parseNumericGroupExpressionWithImplicitMultiplication() + is SquareRootSymbol -> parseNumericRootedTermWithImplicitMultiplication() + // TODO: add error that one of the above was expected. Other error handling should maybe + // happen in the same way. + else -> throw ParseException() + } + } + + private fun hasNextImplicitlyMultipliableWithoutNumberTerm(): Boolean = when (tokens.peek()) { + is FunctionName, is LeftParenthesisSymbol, SquareRootSymbol -> true + else -> false + } + + private fun parseNumber(): MathExpression { + // number = positive_real_number | positive_integer ; + return MathExpression.newBuilder().apply { + constant = when ( + val numberToken = consumeNextTokenMatching { + it is PositiveInteger || it is PositiveRealNumber + } + ) { + is PositiveInteger -> Real.newBuilder().apply { + integer = numberToken.parsedValue + }.build() + is PositiveRealNumber -> Real.newBuilder().apply { + irrational = numberToken.parsedValue + }.build() + else -> throw ParseException() // Something went wrong. + } + }.build() + } + + // TODO: consider consolidating this with other similar implict mult functions to reduce parser. + private fun parseNumericFunctionExpressionWithImplicitMultiplication(): MathExpression { + // numeric_function_expression_with_implicit_multiplication = + // numeric_function_expression + // , [ implicitly_multipliable_with_number_term ] ; + val possibleLhs = parseNumericFunctionExpression() + return if (hasNextImplicitlyMultipliableWithNumberTerm()) { + MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = MathBinaryOperation.Operator.MULTIPLY + leftOperand = possibleLhs + rightOperand = parseImplicitlyMultipliableWithNumberTerm() + }.build() + }.build() + } else possibleLhs + } + + private fun parseNumericFunctionExpression(): MathExpression { + // numeric_function_expression = + // function_name , left_paren , numeric_expression , right_paren ; + return MathExpression.newBuilder().apply { + val functionName = expectNextTokenWithType() + if (functionName.parsedName != "sqrt") throw ParseException() + consumeTokenOfType { LeftParenthesisSymbol } + functionCall = MathFunctionCall.newBuilder().apply { + functionType = MathFunctionCall.FunctionType.SQUARE_ROOT + argument = parseNumericExpression() + }.build() + consumeTokenOfType { RightParenthesisSymbol } + }.build() + } + + private fun parseNumericGroupExpressionWithImplicitMultiplication(): MathExpression { + // numeric_group_expression_with_implicit_multiplication = + // numeric_group_expression , [ implicitly_multipliable_with_number_term ] ; + val possibleLhs = parseNumericGroupExpression() + return if (hasNextImplicitlyMultipliableWithNumberTerm()) { + MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = MathBinaryOperation.Operator.MULTIPLY + leftOperand = possibleLhs + rightOperand = parseImplicitlyMultipliableWithNumberTerm() + }.build() + }.build() + } else possibleLhs + } + + private fun parseNumericGroupExpression(): MathExpression { + // numeric_group_expression = left_paren , numeric_expression , right_paren ; + consumeTokenOfType { LeftParenthesisSymbol } + return parseNumericExpression().also { + consumeTokenOfType { RightParenthesisSymbol } + } + } + + private fun parseNumericPlusMinusUnaryTerm(): MathExpression { + // numeric_plus_minus_unary_term = numeric_negated_term | numeric_positive_term ; + return if (tokens.peek() is MinusSymbol) { + parseNumericNegatedTerm() + } else parseNumericPositiveTerm() + } + + private fun parseNumericNegatedTerm(): MathExpression { + // numeric_negated_term = minus_operator , numeric_term ; + consumeTokenOfType { MinusSymbol } + return MathExpression.newBuilder().apply { + unaryOperation = MathUnaryOperation.newBuilder().apply { + operator = MathUnaryOperation.Operator.NEGATE + operand = parseNumericTerm() + }.build() + }.build() + } + + private fun parseNumericPositiveTerm(): MathExpression { + // numeric_positive_term = plus_operator , numeric_term ; + consumeTokenOfType { PlusSymbol } + return MathExpression.newBuilder().apply { + unaryOperation = MathUnaryOperation.newBuilder().apply { + operator = MathUnaryOperation.Operator.POSITIVE + operand = parseNumericTerm() + }.build() + }.build() + } + + private fun parseNumericRootedTermWithImplicitMultiplication(): MathExpression { + // numeric_rooted_term_with_implicit_multiplication = + // numeric_rooted_term , [ implicitly_multipliable_with_number_term ] ; + val possibleLhs = parseNumericRootedTerm() + return if (hasNextImplicitlyMultipliableWithNumberTerm()) { + MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = MathBinaryOperation.Operator.MULTIPLY + leftOperand = possibleLhs + rightOperand = parseImplicitlyMultipliableWithNumberTerm() + }.build() + }.build() + } else possibleLhs + } + + private fun parseNumericRootedTerm(): MathExpression { + // numeric_rooted_term = square_root_operator , numeric_term ; + consumeTokenOfType { SquareRootSymbol } + return MathExpression.newBuilder().apply { + functionCall = MathFunctionCall.newBuilder().apply { + functionType = MathFunctionCall.FunctionType.SQUARE_ROOT + argument = parseNumericTerm() + }.build() + }.build() + } + + private inline fun expectNextTokenWithType(): T { + return (tokens.next() as? T) ?: throw ParseException() + } + + private inline fun consumeTokenOfType(noinline expected: () -> T): T { + return (tokens.expectNextValue(expected) as? T) ?: throw ParseException() + } + + private fun consumeNextTokenMatching(predicate: (Token) -> Boolean): Token { + return tokens.expectNextMatches(predicate) ?: throw ParseException() + } + + // TODO: do error handling better than this (& in a way that works better with the types of errors + // that we want to show users). + class ParseException : Exception() +} diff --git a/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt b/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt new file mode 100644 index 00000000000..1a7abacc061 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt @@ -0,0 +1,38 @@ +package org.oppia.android.util.math + +class PeekableIterator(private val backingIterator: Iterator) : Iterator { + private var next: T? = null + private var count: Int = 0 + + override fun hasNext(): Boolean = next != null || backingIterator.hasNext() + + override fun next(): T = next?.also { + next = null + count++ + } ?: retrieveNext() + + fun peek(): T? { + return when { + next != null -> next + hasNext() -> retrieveNext().also { next = it } + else -> null + } + } + + fun expectNextValue(expected: () -> T): T? = expectNextMatches { it == expected() } + + fun expectNextMatches(predicate: (T) -> Boolean): T? { + // Only call the predicate if not at the end of the stream, and only call next() if the next + // value matches. + return peek()?.takeIf(predicate)?.also { next() } + } + + fun getRetrievalCount(): Int = count + + private fun retrieveNext(): T = backingIterator.next() + + companion object { + fun fromSequence(sequence: Sequence): PeekableIterator = + PeekableIterator(sequence.iterator()) + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 723a197f223..6813f9ce74e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -2,7 +2,6 @@ TODO: add docs """ -load("@dagger//:workspace_defs.bzl", "dagger_rules") load("//:oppia_android_test.bzl", "oppia_android_test") oppia_android_test( @@ -12,7 +11,6 @@ oppia_android_test( test_class = "org.oppia.android.util.math.MathExpressionParserTest", test_manifest = "//utility:test_manifest", deps = [ - ":dagger", "//model/src/main/proto:math_java_proto_lite", "//testing:assertion_helpers", "//third_party:androidx_test_ext_junit", @@ -31,7 +29,6 @@ oppia_android_test( test_class = "org.oppia.android.util.math.MathTokenizerTest", test_manifest = "//utility:test_manifest", deps = [ - ":dagger", "//model/src/main/proto:math_java_proto_lite", "//testing:assertion_helpers", "//third_party:androidx_test_ext_junit", @@ -43,4 +40,21 @@ oppia_android_test( ], ) -dagger_rules() +oppia_android_test( + name = "NumericExpressionParserTest", + srcs = ["NumericExpressionParserTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.NumericExpressionParserTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing:assertion_helpers", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + "//utility/src/main/java/org/oppia/android/util/math:numeric_expression_parser", + ], +) diff --git a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt index 44cd48d05d2..0203b9eb883 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt @@ -1,25 +1,118 @@ package org.oppia.android.util.math import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.DoubleSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.StringSubject +import com.google.common.truth.Subject +import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.testing.assertThrows -import org.oppia.android.util.math.MathTokenizer.Token.CloseParenthesis -import org.oppia.android.util.math.MathTokenizer.Token.DecimalNumber -import org.oppia.android.util.math.MathTokenizer.Token.Identifier -import org.oppia.android.util.math.MathTokenizer.Token.InvalidIdentifier -import org.oppia.android.util.math.MathTokenizer.Token.InvalidToken -import org.oppia.android.util.math.MathTokenizer.Token.OpenParenthesis -import org.oppia.android.util.math.MathTokenizer.Token.Operator -import org.oppia.android.util.math.MathTokenizer.Token.WholeNumber +import org.oppia.android.util.math.MathTokenizer2.Companion.Token import org.robolectric.annotation.LooperMode /** Tests for [MathTokenizer]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class MathTokenizerTest { - private val ALLOWED_XYZ_VARIABLES = listOf("x", "y", "z") + @Test + fun testLotsOfCases() { + // TODO: split this up + val tokens1 = MathTokenizer2.tokenize(" ").toList() + assertThat(tokens1).isEmpty() + + val tokens2 = MathTokenizer2.tokenize(" 2 ").toList() + assertThat(tokens2).hasSize(1) + assertThat(tokens2.first()).isPositiveIntegerWhoseValue().isEqualTo(2) + + val tokens3 = MathTokenizer2.tokenize(" 2.5 ").toList() + assertThat(tokens3).hasSize(1) + assertThat(tokens3.first()).isPositiveRealNumberWhoseValue().isWithin(1e-5).of(2.5) + + val tokens4 = MathTokenizer2.tokenize(" x ").toList() + assertThat(tokens4).hasSize(1) + assertThat(tokens4.first()).isVariableWhoseName().isEqualTo("x") + + val tokens5 = MathTokenizer2.tokenize(" z x ").toList() + assertThat(tokens5).hasSize(2) + assertThat(tokens5[0]).isVariableWhoseName().isEqualTo("z") + assertThat(tokens5[1]).isVariableWhoseName().isEqualTo("x") + + val tokens6 = MathTokenizer2.tokenize("2^3^2").toList() + assertThat(tokens6).hasSize(5) + assertThat(tokens6[0]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens6[1]).isExponentiationSymbol() + assertThat(tokens6[2]).isPositiveIntegerWhoseValue().isEqualTo(3) + assertThat(tokens6[3]).isExponentiationSymbol() + assertThat(tokens6[4]).isPositiveIntegerWhoseValue().isEqualTo(2) + + val tokens7 = MathTokenizer2.tokenize("sqrt(2)").toList() + assertThat(tokens7).hasSize(4) + assertThat(tokens7[0]).isFunctionWhoseName().isEqualTo("sqrt") + assertThat(tokens7[1]).isLeftParenthesisSymbol() + assertThat(tokens7[2]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens7[3]).isRightParenthesisSymbol() + + val tokens8 = MathTokenizer2.tokenize("sqr(2)").toList() + assertThat(tokens8).hasSize(4) + assertThat(tokens8[0]).isInvalidToken() + assertThat(tokens8[1]).isLeftParenthesisSymbol() + assertThat(tokens8[2]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens8[3]).isRightParenthesisSymbol() + + val tokens9 = MathTokenizer2.tokenize("xyz(2)").toList() + assertThat(tokens9).hasSize(6) + assertThat(tokens9[0]).isVariableWhoseName().isEqualTo("x") + assertThat(tokens9[1]).isVariableWhoseName().isEqualTo("y") + assertThat(tokens9[2]).isVariableWhoseName().isEqualTo("z") + assertThat(tokens9[3]).isLeftParenthesisSymbol() + assertThat(tokens9[4]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens9[5]).isRightParenthesisSymbol() + + val tokens10 = MathTokenizer2.tokenize("732").toList() + assertThat(tokens10).hasSize(1) + assertThat(tokens10.first()).isPositiveIntegerWhoseValue().isEqualTo(732) + + val tokens11 = MathTokenizer2.tokenize("73 2").toList() + assertThat(tokens11).hasSize(2) + assertThat(tokens11[0]).isPositiveIntegerWhoseValue().isEqualTo(73) + assertThat(tokens11[1]).isPositiveIntegerWhoseValue().isEqualTo(2) + + val tokens12 = MathTokenizer2.tokenize("1*2-3+4^7-8/3*2+7").toList() + assertThat(tokens12).hasSize(17) + assertThat(tokens12[0]).isPositiveIntegerWhoseValue().isEqualTo(1) + assertThat(tokens12[1]).isMultiplySymbol() + assertThat(tokens12[2]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens12[3]).isMinusSymbol() + assertThat(tokens12[4]).isPositiveIntegerWhoseValue().isEqualTo(3) + assertThat(tokens12[5]).isPlusSymbol() + assertThat(tokens12[6]).isPositiveIntegerWhoseValue().isEqualTo(4) + assertThat(tokens12[7]).isExponentiationSymbol() + assertThat(tokens12[8]).isPositiveIntegerWhoseValue().isEqualTo(7) + assertThat(tokens12[9]).isMinusSymbol() + assertThat(tokens12[10]).isPositiveIntegerWhoseValue().isEqualTo(8) + assertThat(tokens12[11]).isDivideSymbol() + assertThat(tokens12[12]).isPositiveIntegerWhoseValue().isEqualTo(3) + assertThat(tokens12[13]).isMultiplySymbol() + assertThat(tokens12[14]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens12[15]).isPlusSymbol() + assertThat(tokens12[16]).isPositiveIntegerWhoseValue().isEqualTo(7) + + val tokens13 = MathTokenizer2.tokenize("x = √2 × 7 ÷ 4").toList() + assertThat(tokens13).hasSize(8) + assertThat(tokens13[0]).isVariableWhoseName().isEqualTo("x") + assertThat(tokens13[1]).isEqualsSymbol() + assertThat(tokens13[2]).isSquareRootSymbol() + assertThat(tokens13[3]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens13[4]).isMultiplySymbol() + assertThat(tokens13[5]).isPositiveIntegerWhoseValue().isEqualTo(7) + assertThat(tokens13[6]).isDivideSymbol() + assertThat(tokens13[7]).isPositiveIntegerWhoseValue().isEqualTo(4) + } + + /*private val ALLOWED_XYZ_VARIABLES = listOf("x", "y", "z") private val ALLOWED_XYZ_WITH_LAMBDA_VARIABLES = ALLOWED_XYZ_VARIABLES + listOf("lambda") private val ALLOWED_XYZ_WITH_COMBINED_XYZ_VARIABLES = ALLOWED_XYZ_VARIABLES + listOf("xyz") @@ -546,5 +639,81 @@ class MathTokenizerTest { assertThat(tokens).hasSize(1) assertThat(tokens.first()).isInstanceOf(InvalidToken::class.java) assertThat((tokens.first() as InvalidToken).token).isEqualTo("∫") + }*/ + + private class TokenSubject( + metadata: FailureMetadata, + private val actual: T + ) : Subject(metadata, actual) { + fun isPositiveIntegerWhoseValue(): IntegerSubject { + return assertThat(actual.asVerifiedType().parsedValue) + } + + fun isPositiveRealNumberWhoseValue(): DoubleSubject { + return assertThat(actual.asVerifiedType().parsedValue) + } + + fun isVariableWhoseName(): StringSubject { + return assertThat(actual.asVerifiedType().parsedName) + } + + fun isFunctionWhoseName(): StringSubject { + return assertThat(actual.asVerifiedType().parsedName) + } + + fun isMinusSymbol() { + actual.asVerifiedType() + } + + fun isSquareRootSymbol() { + actual.asVerifiedType() + } + + fun isPlusSymbol() { + actual.asVerifiedType() + } + + fun isMultiplySymbol() { + actual.asVerifiedType() + } + + fun isDivideSymbol() { + actual.asVerifiedType() + } + + fun isExponentiationSymbol() { + actual.asVerifiedType() + } + + fun isEqualsSymbol() { + actual.asVerifiedType() + } + + fun isLeftParenthesisSymbol() { + actual.asVerifiedType() + } + + fun isRightParenthesisSymbol() { + actual.asVerifiedType() + } + + fun isInvalidToken() { + actual.asVerifiedType() + } + + private companion object { + private inline fun Token.asVerifiedType(): T { + assertThat(this).isInstanceOf(T::class.java) + return this as T + } + } + } + + private companion object { + private fun assertThat(actual: T): TokenSubject = + assertAbout(createTokenSubjectFactory()).that(actual) + + private fun createTokenSubjectFactory() = + Subject.Factory, T>(::TokenSubject) } } diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt new file mode 100644 index 00000000000..1fbbd952a47 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -0,0 +1,1096 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.BooleanSubject +import com.google.common.truth.DoubleSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.Subject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.Fraction +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.Real +import org.oppia.android.testing.assertThrows +import org.robolectric.annotation.LooperMode +import kotlin.math.sqrt + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class NumericExpressionParserTest { + @Test + fun testLotsOfCases() { + // TODO: split this up + // TODO: add log string generation for expressions. + expectFailureWhenParsing("") + + val expression1 = parseExpression("1") + assertThat(expression1).hasStructureThatMatches { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + assertThat(expression1).evaluatesToIntegerThat().isEqualTo(1) + + expectFailureWhenParsing("x") + + val expression2 = parseExpression(" 2 ") + assertThat(expression2).hasStructureThatMatches { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + assertThat(expression2).evaluatesToIntegerThat().isEqualTo(2) + + val expression3 = parseExpression(" 2.5 ") + assertThat(expression3).hasStructureThatMatches { + constant { + withIrrationalValueThat().isWithin(1e-5).of(2.5) + } + } + assertThat(expression3).evaluatesToIrrationalThat().isWithin(1e-5).of(2.5) + + expectFailureWhenParsing(" x ") + + expectFailureWhenParsing(" z x ") + + val expression4 = parseExpression("2^3^2") + assertThat(expression4).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression4).evaluatesToIntegerThat().isEqualTo(512) + + val expression23 = parseExpression("(2^3)^2") + assertThat(expression23).hasStructureThatMatches { + exponentiation { + leftOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + assertThat(expression23).evaluatesToIntegerThat().isEqualTo(64) + + val expression24 = parseExpression("512/32/4") + assertThat(expression24).hasStructureThatMatches { + division { + leftOperand { + division { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(512) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(32) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + assertThat(expression24).evaluatesToIntegerThat().isEqualTo(4) + + val expression25 = parseExpression("512/(32/4)") + assertThat(expression25).hasStructureThatMatches { + division { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(512) + } + } + rightOperand { + division { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(32) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression25).evaluatesToIntegerThat().isEqualTo(64) + + val expression5 = parseExpression("sqrt(2)") + assertThat(expression5).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + assertThat(expression5).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0)) + + expectFailureWhenParsing("sqr(2)") + + expectFailureWhenParsing("xyz(2)") + + val expression6 = parseExpression("732") + assertThat(expression6).hasStructureThatMatches { + constant { + withIntegerValueThat().isEqualTo(732) + } + } + assertThat(expression6).evaluatesToIntegerThat().isEqualTo(732) + + expectFailureWhenParsing("73 2") + + val expression7 = parseExpression("3*2-3+4^7*8/3*2+7") + assertThat(expression7).hasStructureThatMatches { + // To better visualize the precedence & order of operations, see this grouped version: + // (((3*2)-3)+((((4^7)*8)/3)*2))+7. + addition { + leftOperand { + // ((3*2)-3)+((((4^7)*8)/3)*2) + addition { + leftOperand { + // (1*2)-3 + subtraction { + leftOperand { + // 3*2 + multiplication { + leftOperand { + // 3 + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + // 2 + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + // 3 + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + rightOperand { + // (((4^7)*8)/3)*2 + multiplication { + leftOperand { + // ((4^7)*8)/3 + division { + leftOperand { + // (4^7)*8 + multiplication { + leftOperand { + // 4^7 + exponentiation { + leftOperand { + // 4 + constant { + withIntegerValueThat().isEqualTo(4) + } + } + rightOperand { + // 7 + constant { + withIntegerValueThat().isEqualTo(7) + } + } + } + } + rightOperand { + // 8 + constant { + withIntegerValueThat().isEqualTo(8) + } + } + } + } + rightOperand { + // 3 + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 2 + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + // 7 + constant { + withIntegerValueThat().isEqualTo(7) + } + } + } + } + assertThat(expression7) + .evaluatesToRationalThat() + .evaluatesToRealThat() + .isWithin(1e-5) + .of(87391.333333333) + + expectFailureWhenParsing("x = √2 × 7 ÷ 4") + + val expression8 = parseExpression("(1+2)(3+4)") + assertThat(expression8).hasStructureThatMatches { + multiplication { + leftOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression8).evaluatesToIntegerThat().isEqualTo(21) + + val expression9 = parseExpression("(1+2)2") + assertThat(expression9).hasStructureThatMatches { + multiplication { + leftOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + assertThat(expression9).evaluatesToIntegerThat().isEqualTo(6) + + val expression10 = parseExpression("2(1+2)") + assertThat(expression10).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression10).evaluatesToIntegerThat().isEqualTo(6) + + val expression11 = parseExpression("sqrt(2)3") + assertThat(expression11).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + assertThat(expression11).evaluatesToIrrationalThat().isWithin(1e-5).of(3.0 * sqrt(2.0)) + + val expression12 = parseExpression("3sqrt(2)") + assertThat(expression12).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression12).evaluatesToIrrationalThat().isWithin(1e-5).of(3.0 * sqrt(2.0)) + + expectFailureWhenParsing("xsqrt(2)") + + // TODO: add version with implicit multiplication (has wrong associativity today). + val expression13 = parseExpression("sqrt(2)*(1+2)*(3-2^5)") + assertThat(expression13).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(5) + } + } + } + } + } + } + } + } + assertThat(expression13).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) + + val expression14 = parseExpression("((3))") + assertThat(expression14).hasStructureThatMatches { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + assertThat(expression14).evaluatesToIntegerThat().isEqualTo(3) + + val expression15 = parseExpression("++3") + assertThat(expression15).hasStructureThatMatches { + positive { + operand { + positive { + operand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + } + } + assertThat(expression15).evaluatesToIntegerThat().isEqualTo(3) + + val expression16 = parseExpression("--4") + assertThat(expression16).hasStructureThatMatches { + negation { + operand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression16).evaluatesToIntegerThat().isEqualTo(4) + + val expression17 = parseExpression("1+-4") + assertThat(expression17).hasStructureThatMatches { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression17).evaluatesToIntegerThat().isEqualTo(-3) + + val expression18 = parseExpression("1++4") + assertThat(expression18).hasStructureThatMatches { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + positive { + operand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression18).evaluatesToIntegerThat().isEqualTo(5) + + val expression19 = parseExpression("1--4") + assertThat(expression19).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression19).evaluatesToIntegerThat().isEqualTo(5) + + expectFailureWhenParsing("1-^-4") + + val expression20 = parseExpression("√2 × 7 ÷ 4") + assertThat(expression20).hasStructureThatMatches { + division { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(7) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + assertThat(expression20).evaluatesToIrrationalThat().isWithin(1e-5).of((sqrt(2.0) * 7.0) / 4.0) + + expectFailureWhenParsing("1+2 asdf") + + val expression21 = parseExpression("sqrt(2)sqrt(3)sqrt(4)") + assertThat(expression21).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + } + } + assertThat(expression21) + .evaluatesToIrrationalThat() + .isWithin(1e-5) + .of(sqrt(2.0) * sqrt(3.0) * sqrt(4.0)) + + val expression22 = parseExpression("(1+2)(3-7^2)(5+-17)") + assertThat(expression22).hasStructureThatMatches { + multiplication { + leftOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + multiplication { + leftOperand { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(7) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(5) + } + } + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(17) + } + } + } + } + } + } + } + } + } + } + assertThat(expression22).evaluatesToIntegerThat().isEqualTo(1656) + + val expression26 = parseExpression("3^-2") + assertThat(expression26).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression26).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(9) + } + + val expression27 = parseExpression("(3^-2)^(3^-2)") + assertThat(expression27).hasStructureThatMatches { + exponentiation { + leftOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + } + } + assertThat(expression27).evaluatesToIrrationalThat().isWithin(1e-5).of(0.78338103693) + + val expression28 = parseExpression("1-3^sqrt(4)") + assertThat(expression28).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + } + } + assertThat(expression28).evaluatesToIntegerThat().isEqualTo(-8) + + // "Hard" order of operation problems loosely based on & other problems that can often stump + // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. + // TODO: test implicit version (currently broken in parser) + val expression29 = parseExpression("3÷2*(3+4)") + assertThat(expression29).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression29).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) + + // TODO: add others + } + + @DslMarker + private annotation class ExpressionComparatorMarker + + // See: https://kotlinlang.org/docs/type-safe-builders.html. + private class MathExpressionSubject( + metadata: FailureMetadata, + private val actual: MathExpression + ) : Subject(metadata, actual) { + fun hasStructureThatMatches(init: ExpressionComparator.() -> Unit): ExpressionComparator { + // TODO: maybe verify that all aspects are verified? + return ExpressionComparator.createFromExpression(actual).also(init) + } + + fun evaluatesToRationalThat(): FractionSubject = + assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.RATIONAL).rational) + + fun evaluatesToIrrationalThat(): DoubleSubject = + assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.IRRATIONAL).irrational) + + fun evaluatesToIntegerThat(): IntegerSubject = + assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.INTEGER).integer) + + private fun evaluateAsReal(expectedType: Real.RealTypeCase): Real { + val real = actual.evaluateAsNumericExpression() + assertWithMessage("Failed to evaluate numeric expression").that(real).isNotNull() + assertWithMessage("Expected constant to evaluate to $expectedType") + .that(real?.realTypeCase) + .isEqualTo(expectedType) + return checkNotNull(real) // Just to remove the nullable operator; the actual check is above. + } + + @ExpressionComparatorMarker + class ExpressionComparator private constructor(private val expression: MathExpression) { + // TODO: convert to constant comparator? + fun constant(init: ConstantComparator.() -> Unit): ConstantComparator = + ConstantComparator.createFromExpression(expression).also(init) + + fun addition(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.ADD + ).also(init) + } + + fun subtraction(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.SUBTRACT + ).also(init) + } + + fun multiplication(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.MULTIPLY + ).also(init) + } + + fun division(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.DIVIDE + ).also(init) + } + + fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.EXPONENTIATE + ).also(init) + } + + fun negation(init: UnaryOperationComparator.() -> Unit): UnaryOperationComparator { + return UnaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathUnaryOperation.Operator.NEGATE + ).also(init) + } + + fun positive(init: UnaryOperationComparator.() -> Unit): UnaryOperationComparator { + return UnaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathUnaryOperation.Operator.POSITIVE + ).also(init) + } + + fun functionCallTo( + type: MathFunctionCall.FunctionType, + init: FunctionCallComparator.() -> Unit + ): FunctionCallComparator { + return FunctionCallComparator.createFromExpression( + expression, + expectedFunctionType = type + ).also(init) + } + + internal companion object { + fun createFromExpression(expression: MathExpression): ExpressionComparator = + ExpressionComparator(expression) + } + } + + @ExpressionComparatorMarker + class ConstantComparator private constructor(private val constant: Real) { + fun withIntegerValueThat(): IntegerSubject { + assertThat(constant.realTypeCase).isEqualTo(Real.RealTypeCase.INTEGER) + return assertThat(constant.integer) + } + + fun withIrrationalValueThat(): DoubleSubject { + assertThat(constant.realTypeCase).isEqualTo(Real.RealTypeCase.IRRATIONAL) + return assertThat(constant.irrational) + } + + internal companion object { + fun createFromExpression(expression: MathExpression): ConstantComparator { + assertThat(expression.expressionTypeCase).isEqualTo(CONSTANT) + return ConstantComparator(expression.constant) + } + } + } + + @ExpressionComparatorMarker + class BinaryOperationComparator private constructor( + private val operation: MathBinaryOperation + ) { + fun leftOperand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(operation.leftOperand).also(init) + + fun rightOperand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(operation.rightOperand).also(init) + + internal companion object { + fun createFromExpression( + expression: MathExpression, + expectedOperator: MathBinaryOperation.Operator + ): BinaryOperationComparator { + assertThat(expression.expressionTypeCase).isEqualTo(BINARY_OPERATION) + assertWithMessage("Expected binary operation with operator: $expectedOperator") + .that(expression.binaryOperation.operator) + .isEqualTo(expectedOperator) + return BinaryOperationComparator(expression.binaryOperation) + } + } + } + + @ExpressionComparatorMarker + class UnaryOperationComparator private constructor( + private val operation: MathUnaryOperation + ) { + fun operand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(operation.operand).also(init) + + internal companion object { + fun createFromExpression( + expression: MathExpression, + expectedOperator: MathUnaryOperation.Operator + ): UnaryOperationComparator { + assertThat(expression.expressionTypeCase).isEqualTo(UNARY_OPERATION) + assertWithMessage("Expected unary operation with operator: $expectedOperator") + .that(expression.unaryOperation.operator) + .isEqualTo(expectedOperator) + return UnaryOperationComparator(expression.unaryOperation) + } + } + } + + @ExpressionComparatorMarker + class FunctionCallComparator private constructor( + private val functionCall: MathFunctionCall + ) { + fun argument(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(functionCall.argument).also(init) + + internal companion object { + fun createFromExpression( + expression: MathExpression, + expectedFunctionType: MathFunctionCall.FunctionType + ): FunctionCallComparator { + assertThat(expression.expressionTypeCase).isEqualTo(FUNCTION_CALL) + assertWithMessage("Expected function call to: $expectedFunctionType") + .that(expression.functionCall.functionType) + .isEqualTo(expectedFunctionType) + return FunctionCallComparator(expression.functionCall) + } + } + } + } + + // TODO: move this to a common location. + private class FractionSubject( + metadata: FailureMetadata, + private val actual: Fraction + ) : Subject(metadata, actual) { + fun hasNegativePropertyThat(): BooleanSubject = assertThat(actual.isNegative) + + fun hasWholeNumberThat(): IntegerSubject = assertThat(actual.wholeNumber) + + fun hasNumeratorThat(): IntegerSubject = assertThat(actual.numerator) + + fun hasDenominatorThat(): IntegerSubject = assertThat(actual.denominator) + + fun evaluatesToRealThat(): DoubleSubject = assertThat(actual.toDouble()) + } + + private companion object { + private fun expectFailureWhenParsing(expression: String) { + assertThrows(NumericExpressionParser.ParseException::class) { parseExpression(expression) } + } + + private fun parseExpression(expression: String): MathExpression { + return NumericExpressionParser(expression).parse() + } + + private fun assertThat(actual: MathExpression): MathExpressionSubject = + assertAbout(::MathExpressionSubject).that(actual) + + private fun assertThat(actual: Fraction): FractionSubject = + assertAbout(::FractionSubject).that(actual) + } +} From 7ca166085a99808dbaeb3d83b1e17accdf94a89a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 9 Nov 2021 21:15:29 -0800 Subject: [PATCH 072/289] Add implicit multiplication & fix unary. This introduces proper implicit multiplication support (such that it's the same precedence as explicit multiplication/division, and lower precedence than exponentiation). It also fixes unary negation/positive operators order-of-operations. Finally, it specifically disallows numbers being used for right implicit multiplication (since this should never be allowed, and actually supporting it is quite complex). This includes some extra code from previous iterations in attempting more robust implicit multiplication support. That code will be cleaned up in a subsequent commit; it's being checked in for record keeping. --- .../util/math/NumericExpressionParser.kt | 651 +++++++++-- .../util/math/NumericExpressionParserTest.kt | 1027 +++++++++++++++-- 2 files changed, 1496 insertions(+), 182 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt index 37e58953ac2..418c2328a03 100644 --- a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt @@ -25,8 +25,8 @@ class NumericExpressionParser(private val rawExpression: String) { fun parse(): MathExpression { return parseNumericExpression().also { - // Make sure all tokens were consumed (otherwise there are trailing tokens which invalidate the - // whole expression). + // Make sure all tokens were consumed (otherwise there are trailing tokens which invalidate + // the whole expression). if (tokens.hasNext()) throw ParseException() } } @@ -81,7 +81,7 @@ class NumericExpressionParser(private val rawExpression: String) { private fun parseNumericMultDivExpression(): MathExpression { // numeric_mult_div_expression = - // numeric_exp_expression , { numeric_mult_div_expression_rhs } ; + // numeric_implicit_mult_expression , { numeric_mult_div_expression_rhs } ; var lastLhs = parseNumericExpExpression() while (hasNextNumericMultDivExpressionRhs()) { // numeric_mult_div_expression_rhs = @@ -90,7 +90,7 @@ class NumericExpressionParser(private val rawExpression: String) { is MultiplySymbol -> MathBinaryOperation.Operator.MULTIPLY to parseNumericMultExpressionRhs() is DivideSymbol -> MathBinaryOperation.Operator.DIVIDE to parseNumericDivExpressionRhs() - else -> throw ParseException() + else -> MathBinaryOperation.Operator.MULTIPLY to parseNumericImplicitMultExpressionRhs() } // Compute the next LHS if there is further multiplication/division. @@ -106,26 +106,53 @@ class NumericExpressionParser(private val rawExpression: String) { } private fun hasNextNumericMultDivExpressionRhs() = when (tokens.peek()) { - is MultiplySymbol, is DivideSymbol -> true + is MultiplySymbol, is DivideSymbol, is FunctionName, is LeftParenthesisSymbol, + is SquareRootSymbol -> true else -> false } private fun parseNumericMultExpressionRhs(): MathExpression { // numeric_mult_expression_rhs = - // multiplication_operator , numeric_exp_expression ; + // multiplication_operator , numeric_implicit_mult_expression ; consumeTokenOfType { MultiplySymbol } return parseNumericExpExpression() } private fun parseNumericDivExpressionRhs(): MathExpression { - // numeric_div_expression_rhs = division_operator , numeric_exp_expression ; + // numeric_div_expression_rhs = + // division_operator , numeric_implicit_mult_expression ; consumeTokenOfType { DivideSymbol } return parseNumericExpExpression() } - private fun parseNumericExpExpression(): MathExpression { - // numeric_exp_expression = numeric_term , [ numeric_exp_expression_tail ] ; - val possibleLhs = parseNumericTerm() + private fun parseNumericImplicitMultExpressionRhs(): MathExpression { + return parseNumericTermWithoutUnaryWithoutNumber() + } + + /*private fun parseNumericImplicitMultExpression(): MathExpression { + // numeric_implicit_mult_expression = + // numeric_exp_expression | numeric_term_implicit_mult_expression ; + // TODO: fix + val possibleLhs = parseNumericTermWithoutImplicitMultWithNumberWithUnaryPlusMinus() + return when { + tokens.peek() is ExponentiationSymbol -> + parseNumericExpExpression(numericTermWithoutImplicitMultWithNumberWithUnary = possibleLhs) + hasNextNumericTermImplicitMultExpression() -> + parseNumericTermImplicitMultExpression( + numericTermWithoutImplicitMultWithNumberOrUnaryPlusMinus = possibleLhs + ) + else -> possibleLhs // Nothing follows the term in this production rule. + } + } + + private fun parseNumericExpExpression( + numericTermWithoutImplicitMultWithNumberWithUnary: MathExpression + ): MathExpression { + // numeric_exp_expression = + // numeric_term_without_implicit_mult_with_number_with_unary_plus_minus + // , [ numeric_exp_expression_tail ] ; + @Suppress("UnnecessaryVariable") // The variable adds extra context for readability. + val possibleLhs = numericTermWithoutImplicitMultWithNumberWithUnary return if (tokens.peek() is ExponentiationSymbol) { parseNumericExpExpressionTail(possibleLhs) } else possibleLhs @@ -141,71 +168,296 @@ class NumericExpressionParser(private val rawExpression: String) { binaryOperation = MathBinaryOperation.newBuilder().apply { operator = MathBinaryOperation.Operator.EXPONENTIATE leftOperand = lhs - rightOperand = parseNumericExpExpression() + rightOperand = + parseNumericExpExpression( + parseNumericTermWithoutImplicitMultWithNumberWithUnaryPlusMinus() + ) + }.build() + }.build() + } + + private fun parseNumericTermImplicitMultExpression(): MathExpression { + // numeric_term_implicit_mult_expression = + // numeric_term_implicit_mult_left_number_expansion_expression + // | numeric_term_implicit_mult_right_number_expansion_expression ; + + // TODO: verify associativity and maybe flip it since the recursion probably will result in + // right associativity. + return when { + hasNextNumber() -> parseNumericTermImplicitMultLeftNumberExpansionExpression() + else -> parseNumericTermImplicitMultRightNumberExpansionExpression() + } + +// var lastLhs = parseNumericTermImplicitMultInitialSubexpression().toStandaloneExpression() +// while (hasNextNumericTermImplicitMultLaterSubexpression()) { +// val operands = parseNumericTermWithoutImplicitMultWithoutNumberOrUnaryPlusMinus() +// } +// do { + // Compute the next LHS if there is further implicit multiplication. +// lastLhs = MathExpression.newBuilder().apply { +// binaryOperation = MathBinaryOperation.newBuilder().apply { +// operator = MathBinaryOperation.Operator.MULTIPLY +// leftOperand = lastLhs +// rightOperand = rhs +// }.build() +// }.build() +// } while (hasNextNumericTermWithoutImplicitMultWithoutNumber()) +// return lastLhs + } + + private fun parseNumericTermImplicitMultLeftNumberExpansionExpression(): MathExpression { + // numeric_term_implicit_mult_left_number_expansion_expression = + // numeric_term_implicit_mult_expression_with_number_initial_lhs + // , numeric_term_implicit_mult_without_number_expression_tail ; + // TODO: consider consolidating this with below. + return MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = MathBinaryOperation.Operator.MULTIPLY + leftOperand = parseNumericTermImplicitMultExpressionWithNumberInitialLhs() + rightOperand = parseNumericTermImplicitMultWithoutNumberExpressionTail() }.build() }.build() } - private fun parseNumericTerm(): MathExpression { - // numeric_term = - // implicitly_multipliable_with_number_term | numeric_plus_minus_unary_term ; - return if (hasNextImplicitlyMultipliableWithNumberTerm()) { - parseImplicitlyMultipliableWithNumberTerm() - } else parseNumericPlusMinusUnaryTerm() + private fun parseNumericTermImplicitMultRightNumberExpansionExpression(): MathExpression { + // numeric_term_implicit_mult_right_number_expansion_expression = + // numeric_term_implicit_mult_expression_without_number_initial_lhs + // , numeric_term_implicit_mult_with_number_expression_tail ; + return MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = MathBinaryOperation.Operator.MULTIPLY + leftOperand = parseNumericTermImplicitMultExpressionWithoutNumberInitialLhs() + rightOperand = parseNumericTermImplicitMultWithNumberExpressionTail() + }.build() + }.build() } - private fun parseImplicitlyMultipliableWithNumberTerm(): MathExpression { - // implicitly_multipliable_with_number_term = - // number_with_implicit_multiplication - // | implicitly_multipliable_without_number_term ; - return when (tokens.peek()) { - is PositiveInteger, is PositiveRealNumber -> parseNumberWithImplicitMultiplication() - else -> parseImplicitlyMultipliableWithoutNumberTerm() +// private fun parseNumericTermImplicitMultInitialSubexpression(): MultiplicationOperands { + // numeric_term_implicit_mult_initial_subexpression = + // numeric_term_implicit_mult_initial_left_number_subexpression + // | numeric_term_implicit_mult_initial_right_number_subexpression ; +// return if (hasNextNumber()) { +// parseNumericTermImplicitMultInitialLeftNumberSubexpression() +// } else parseNumericTermImplicitMultInitialRightNumberSubexpression() +// } + +// private fun parseNumericTermImplicitMultInitialLeftNumberSubexpression(): MultiplicationOperands { + // numeric_term_implicit_mult_initial_left_number_subexpression = + // numeric_term_implicit_mult_expression_with_number_initial_lhs + // , numeric_term_implicit_mult_expression_without_number_rhs ; +// return MultiplicationOperands( +// first = parseNumericTermImplicitMultExpressionWithNumberInitialLhs(), +// second = parseNumericTermImplicitMultExpressionWithoutNumberRhs() +// ) +// } + +// private fun parseNumericTermImplicitMultInitialRightNumberSubexpression(): MultiplicationOperands { + // numeric_term_implicit_mult_initial_right_number_subexpression = + // numeric_term_implicit_mult_expression_without_number_initial_lhs + // , numeric_term_implicit_mult_expression_with_number_rhs ; +// return MultiplicationOperands( +// first = parseNumericTermImplicitMultExpressionWithoutNumberInitialLhs(), +// second = parseNumericTermImplicitMultExpressionWithNumberRhs() +// ) +// } + +// private fun parseNumericTermImplicitMultLaterSubexpression(): MultiplicationOperands { + // numeric_term_implicit_mult_later_subexpression = + // numeric_term_implicit_mult_later_left_number_subexpression + // | numeric_term_implicit_mult_later_right_number_subexpression ; +// return if (hasNextNumber()) { +// parseNumericTermImplicitMultLaterLeftNumberSubexpression() +// } else parseNumericTermImplicitMultLaterRightNumberSubexpression() +// } + +// private fun parseNumericTermImplicitMultLaterLeftNumberSubexpression(): MultiplicationOperands { + // numeric_term_implicit_mult_later_left_number_subexpression = + // numeric_term_implicit_mult_expression_with_number_later_lhs + // , numeric_term_implicit_mult_expression_without_number_rhs ; +// return MultiplicationOperands( +// first = parseNumericTermImplicitMultExpressionWithNumberLaterLhs(), +// second = parseNumericTermImplicitMultExpressionWithoutNumberRhs() +// ) +// } + + // TODO: consider consolidating this with the other implicit multiplication cases. +// private fun parseNumericTermImplicitMultLaterRightNumberSubexpression(): MultiplicationOperands { + // numeric_term_implicit_mult_later_right_number_subexpression = + // numeric_term_implicit_mult_expression_without_number_later_lhs + // , numeric_term_implicit_mult_expression_with_number_rhs ; +// return MultiplicationOperands( +// first = parseNumericTermImplicitMultExpressionWithoutNumberLaterLhs(), +// second = parseNumericTermImplicitMultExpressionWithNumberRhs() +// ) +// } + + private fun parseNumericTermImplicitMultWithoutNumberExpressionTail(): MathExpression { + // numeric_term_implicit_mult_without_number_expression_tail = + // numeric_term_implicit_mult_expression_without_number_rhs ; + return parseNumericTermImplicitMultExpressionWithoutNumberRhs() + } + + private fun parseNumericTermImplicitMultWithNumberExpressionTail(): MathExpression { + // numeric_term_implicit_mult_with_number_expression_tail = + // numeric_term_implicit_mult_expression_with_number_rhs ; + return parseNumericTermImplicitMultExpressionWithNumberRhs() + } + + // TODO: consider inlining these for simplicity. + private fun parseNumericTermImplicitMultExpressionWithNumberInitialLhs(): MathExpression { + // numeric_term_implicit_mult_expression_with_number_initial_lhs = + // numeric_term_without_implicit_mult_with_number_with_unary_plus_minus ; + return parseNumericTermWithoutImplicitMultWithNumberWithUnaryPlusMinus() + } + + private fun parseNumericTermImplicitMultExpressionWithoutNumberInitialLhs(): MathExpression { + // numeric_term_implicit_mult_expression_without_number_initial_lhs = + // numeric_term_without_implicit_mult_no_number_with_unary_plus_minus ; + return parseNumericTermWithoutImplicitMultNoNumberWithUnaryPlusMinus() + } + + private fun parseNumericTermImplicitMultExpressionWithNumberLaterLhs(): MathExpression { + // numeric_term_implicit_mult_expression_with_number_later_lhs = + // number | numeric_term_implicit_mult_expression_without_number_later_lhs ; + return when { + hasNextNumber() -> parseNumber() + else -> parseNumericTermImplicitMultExpressionWithoutNumberLaterLhs() } } - private fun hasNextImplicitlyMultipliableWithNumberTerm(): Boolean = when (tokens.peek()) { - is PositiveInteger, is PositiveRealNumber, is FunctionName, is LeftParenthesisSymbol, - is SquareRootSymbol -> true - else -> false + private fun parseNumericTermImplicitMultExpressionWithoutNumberLaterLhs(): MathExpression { + // numeric_term_implicit_mult_expression_without_number_later_lhs = + // numeric_term_without_implicit_mult_no_number_no_unary_plus_minus ; + return parseNumericTermWithoutImplicitMultNoNumberNoUnaryPlusMinus() } - private fun parseNumberWithImplicitMultiplication(): MathExpression { - // number_with_implicit_multiplication = - // number , [ implicitly_multipliable_without_number_term ] ; - // TODO: add specification for implicit multiplication (for reconstruction). - val realOperand = parseNumber() - return if (hasNextImplicitlyMultipliableWithoutNumberTerm()) { + private fun parseNumericTermImplicitMultExpressionWithNumberRhs(): MathExpression { + // numeric_term_implicit_mult_expression_with_number_rhs = + // ( number | numeric_term_without_implicit_mult_no_number_no_unary_plus_minus ) + // , [ numeric_term_implicit_mult_without_number_expression_tail ] ; + val possibleLhs = when { + hasNextNumber() -> parseNumber() + else -> parseNumericTermImplicitMultExpressionWithoutNumberRhs() + } + // TODO: consider consolidating this with the other rhs method. + return if (hasNextNumericTermImplicitMultWithoutNumberExpressionTail()) { MathExpression.newBuilder().apply { binaryOperation = MathBinaryOperation.newBuilder().apply { operator = MathBinaryOperation.Operator.MULTIPLY - leftOperand = realOperand - rightOperand = parseImplicitlyMultipliableWithoutNumberTerm() + leftOperand = possibleLhs + rightOperand = parseNumericTermImplicitMultWithoutNumberExpressionTail() }.build() }.build() - } else realOperand + } else possibleLhs } - private fun parseImplicitlyMultipliableWithoutNumberTerm(): MathExpression { - // implicitly_multipliable_without_number_term = - // numeric_function_expression_with_implicit_multiplication - // | numeric_group_expression_with_implicit_multiplication - // | numeric_rooted_term_with_implicit_multiplication ; + private fun parseNumericTermImplicitMultExpressionWithoutNumberRhs(): MathExpression { + // numeric_term_implicit_mult_expression_without_number_rhs = + // numeric_term_without_implicit_mult_no_number_no_unary_plus_minus + // , [ numeric_term_implicit_mult_with_number_expression_tail ] ; + val possibleLhs = parseNumericTermWithoutImplicitMultNoNumberNoUnaryPlusMinus() + return if (hasNextNumericTermImplicitMultWithNumberExpressionTail()) { + MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = MathBinaryOperation.Operator.MULTIPLY + leftOperand = possibleLhs + rightOperand = parseNumericTermImplicitMultWithNumberExpressionTail() + }.build() + }.build() + } else possibleLhs + } + + private fun parseNumericTermWithoutImplicitMultWithNumberWithUnaryPlusMinus(): MathExpression { + // numeric_term_without_implicit_mult_with_number_with_unary_plus_minus = + // number + // | numeric_term_without_implicit_mult_no_number_no_unary_plus_minus + // | numeric_plus_minus_unary_term_with_number ; return when (tokens.peek()) { - is FunctionName -> parseNumericFunctionExpressionWithImplicitMultiplication() - is LeftParenthesisSymbol -> parseNumericGroupExpressionWithImplicitMultiplication() - is SquareRootSymbol -> parseNumericRootedTermWithImplicitMultiplication() - // TODO: add error that one of the above was expected. Other error handling should maybe - // happen in the same way. - else -> throw ParseException() + is PositiveInteger, is PositiveRealNumber -> parseNumber() + is PlusSymbol, is MinusSymbol -> parseNumericPlusMinusUnaryTermWithNumber() + else -> parseNumericTermWithoutImplicitMultNoNumberNoUnaryPlusMinus() } } - private fun hasNextImplicitlyMultipliableWithoutNumberTerm(): Boolean = when (tokens.peek()) { - is FunctionName, is LeftParenthesisSymbol, SquareRootSymbol -> true - else -> false + private fun parseNumericTermWithoutImplicitMultNoNumberWithUnaryPlusMinus(): MathExpression { + // numeric_term_without_implicit_mult_no_number_with_unary_plus_minus = + // numeric_term_without_implicit_mult_no_number_no_unary_plus_minus + // | numeric_plus_minus_unary_term_without_number ; + return when (tokens.peek()) { + is PlusSymbol, is MinusSymbol -> parseNumericPlusMinusUnaryTermWithoutNumber() + else -> parseNumericTermWithoutImplicitMultNoNumberNoUnaryPlusMinus() + } } + private fun parseNumericTermWithoutImplicitMultNoNumberNoUnaryPlusMinus(): MathExpression { + // numeric_term_without_implicit_mult_no_number_no_unary_plus_minus = + // numeric_function_expression + // | numeric_group_expression + // | numeric_rooted_term ; + return when (tokens.peek()) { + is FunctionName -> parseNumericFunctionExpression() + is LeftParenthesisSymbol -> parseNumericGroupExpression() + is SquareRootSymbol -> parseNumericRootedTerm() + else -> throw ParseException() + } + } + +// private fun parseNumericTermWithImplicitMult( +// numericTermWithoutImplicitMultWithNumberOrUnaryPlusMinus: MathExpression +// ): MathExpression { + // numeric_term_with_implicit_mult = + // numeric_term_implicit_mult_expression_lhs + // , { numeric_term_implicit_mult_expression_rhs }- ; + // numeric_term_implicit_mult_expression_lhs = + // numeric_term_without_implicit_mult_with_number_or_unary_plus_minus +// var lastLhs = numericTermWithoutImplicitMultWithNumberOrUnaryPlusMinus +// do { + // numeric_term_implicit_mult_expression_rhs = + // numeric_term_without_implicit_mult_without_number_or_unary_plus_minus +// val rhs = parseNumericTermWithoutImplicitMultWithoutNumberOrUnaryPlusMinus() +// + // Compute the next LHS if there is further implicit multiplication. +// lastLhs = MathExpression.newBuilder().apply { +// binaryOperation = MathBinaryOperation.newBuilder().apply { +// operator = MathBinaryOperation.Operator.MULTIPLY +// leftOperand = lastLhs +// rightOperand = rhs +// }.build() +// }.build() +// } while (hasNextNumericTermWithoutImplicitMultWithoutNumber()) +// return lastLhs +// } + +// private fun parseNumericTermWithoutImplicitMultWithNumberOrUnaryPlusMinus(): MathExpression { + // numeric_term_without_implicit_mult_with_number_or_unary_plus_minus = + // number + // | numeric_term_without_implicit_mult_without_number + // | numeric_plus_minus_unary_term; +// return when (tokens.peek()) { +// is PositiveInteger, is PositiveRealNumber -> parseNumber() +// is PlusSymbol, is MinusSymbol -> parseNumericPlusMinusUnaryTerm() +// else -> parseNumericTermWithoutImplicitMultWithoutNumberOrUnaryPlusMinus() +// } +// } + +// private fun parseNumericTermWithoutImplicitMultWithoutNumberOrUnaryPlusMinus(): MathExpression { + // numeric_term_without_implicit_mult_without_number_or_unary_plus_minus = + // numeric_function_expression + // | numeric_group_expression + // | numeric_rooted_term; +// return when (tokens.peek()) { +// is FunctionName -> parseNumericFunctionExpression() +// is LeftParenthesisSymbol -> parseNumericGroupExpression() +// is SquareRootSymbol -> parseNumericRootedTerm() +// else -> throw ParseException() +// } +// } + +// private fun hasNextNumericTermWithoutImplicitMultWithoutNumber(): Boolean = when (tokens.peek()) { +// is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> true +// else -> false +// } + private fun parseNumber(): MathExpression { // number = positive_real_number | positive_integer ; return MathExpression.newBuilder().apply { @@ -220,26 +472,17 @@ class NumericExpressionParser(private val rawExpression: String) { is PositiveRealNumber -> Real.newBuilder().apply { irrational = numberToken.parsedValue }.build() + + // TODO: add error that one of the above was expected. Other error handling should maybe + // happen in the same way. else -> throw ParseException() // Something went wrong. } }.build() } - // TODO: consider consolidating this with other similar implict mult functions to reduce parser. - private fun parseNumericFunctionExpressionWithImplicitMultiplication(): MathExpression { - // numeric_function_expression_with_implicit_multiplication = - // numeric_function_expression - // , [ implicitly_multipliable_with_number_term ] ; - val possibleLhs = parseNumericFunctionExpression() - return if (hasNextImplicitlyMultipliableWithNumberTerm()) { - MathExpression.newBuilder().apply { - binaryOperation = MathBinaryOperation.newBuilder().apply { - operator = MathBinaryOperation.Operator.MULTIPLY - leftOperand = possibleLhs - rightOperand = parseImplicitlyMultipliableWithNumberTerm() - }.build() - }.build() - } else possibleLhs + private fun hasNextNumber(): Boolean = when (tokens.peek()) { + is PositiveInteger, is PositiveRealNumber -> true + else -> false } private fun parseNumericFunctionExpression(): MathExpression { @@ -257,19 +500,229 @@ class NumericExpressionParser(private val rawExpression: String) { }.build() } - private fun parseNumericGroupExpressionWithImplicitMultiplication(): MathExpression { - // numeric_group_expression_with_implicit_multiplication = - // numeric_group_expression , [ implicitly_multipliable_with_number_term ] ; - val possibleLhs = parseNumericGroupExpression() - return if (hasNextImplicitlyMultipliableWithNumberTerm()) { - MathExpression.newBuilder().apply { + private fun parseNumericGroupExpression(): MathExpression { + // numeric_group_expression = left_paren , numeric_expression , right_paren ; + consumeTokenOfType { LeftParenthesisSymbol } + return parseNumericExpression().also { + consumeTokenOfType { RightParenthesisSymbol } + } + } + + private fun parseNumericRootedTerm(): MathExpression { + // numeric_rooted_term = + // square_root_operator + // , numeric_term_without_implicit_mult_with_number_with_unary_plus_minus ; + consumeTokenOfType { SquareRootSymbol } + return MathExpression.newBuilder().apply { + functionCall = MathFunctionCall.newBuilder().apply { + functionType = MathFunctionCall.FunctionType.SQUARE_ROOT + argument = parseNumericTermWithoutImplicitMultWithNumberWithUnaryPlusMinus() + }.build() + }.build() + } + + private fun parseNumericPlusMinusUnaryTermWithNumber(): MathExpression { + // numeric_plus_minus_unary_term_with_number = + // numeric_negated_term_with_number | numeric_positive_term_with_number ; + return if (tokens.peek() is MinusSymbol) { + parseNumericNegatedTermWithNumber() + } else parseNumericPositiveTermWithNumber() + } + + private fun parseNumericPlusMinusUnaryTermWithoutNumber(): MathExpression { + // numeric_plus_minus_unary_term_without_number = + // numeric_negated_term_without_number + // | numeric_positive_term_without_number ; + return if (tokens.peek() is MinusSymbol) { + parseNumericNegatedTermWithoutNumber() + } else parseNumericPositiveTermWithoutNumber() + } + + // TODO: consider consolidating the similar negated/positive methods. + private fun parseNumericNegatedTermWithNumber(): MathExpression { + // numeric_negated_term_with_number = + // minus_operator + // , numeric_term_without_implicit_mult_with_number_with_unary_plus_minus ; + consumeTokenOfType { MinusSymbol } + return MathExpression.newBuilder().apply { + unaryOperation = MathUnaryOperation.newBuilder().apply { + operator = MathUnaryOperation.Operator.NEGATE + operand = parseNumericTermWithoutImplicitMultWithNumberWithUnaryPlusMinus() + }.build() + }.build() + } + + private fun parseNumericNegatedTermWithoutNumber(): MathExpression { + // numeric_negated_term_without_number = + // minus_operator + // , numeric_term_without_implicit_mult_no_number_no_unary_plus_minus ; + consumeTokenOfType { MinusSymbol } + return MathExpression.newBuilder().apply { + unaryOperation = MathUnaryOperation.newBuilder().apply { + operator = MathUnaryOperation.Operator.NEGATE + operand = parseNumericTermWithoutImplicitMultNoNumberNoUnaryPlusMinus() + }.build() + }.build() + } + + private fun parseNumericPositiveTermWithNumber(): MathExpression { + // numeric_positive_term_with_number = + // plus_operator + // , numeric_term_without_implicit_mult_with_number_with_unary_plus_minus ; + consumeTokenOfType { PlusSymbol } + return MathExpression.newBuilder().apply { + unaryOperation = MathUnaryOperation.newBuilder().apply { + operator = MathUnaryOperation.Operator.POSITIVE + operand = parseNumericTermWithoutImplicitMultWithNumberWithUnaryPlusMinus() + }.build() + }.build() + } + + private fun parseNumericPositiveTermWithoutNumber(): MathExpression { + // numeric_positive_term_without_number = + // plus_operator + // , numeric_term_without_implicit_mult_no_number_no_unary_plus_minus ; + consumeTokenOfType { PlusSymbol } + return MathExpression.newBuilder().apply { + unaryOperation = MathUnaryOperation.newBuilder().apply { + operator = MathUnaryOperation.Operator.POSITIVE + operand = parseNumericTermWithoutImplicitMultNoNumberNoUnaryPlusMinus() + }.build() + }.build() + } + +// private fun parseNumericPlusMinusUnaryTerm(): MathExpression { + // numeric_plus_minus_unary_term = numeric_negated_term | numeric_positive_term ; +// return if (tokens.peek() is MinusSymbol) { +// parseNumericNegatedTerm() +// } else parseNumericPositiveTerm() +// } + +// private fun parseNumericNegatedTerm(): MathExpression { + // numeric_negated_term = + // minus_operator , numeric_term_without_implicit_mult_with_number_or_unary_plus_minus ; +// consumeTokenOfType { MinusSymbol } +// return MathExpression.newBuilder().apply { +// unaryOperation = MathUnaryOperation.newBuilder().apply { +// operator = MathUnaryOperation.Operator.NEGATE +// operand = parseNumericTermWithoutImplicitMultWithNumberOrUnaryPlusMinus() +// }.build() +// }.build() +// } + +// private fun parseNumericPositiveTerm(): MathExpression { + // numeric_positive_term = + // plus_operator , numeric_term_without_implicit_mult_with_number_or_unary_plus_minus ; +// consumeTokenOfType { PlusSymbol } +// return MathExpression.newBuilder().apply { +// unaryOperation = MathUnaryOperation.newBuilder().apply { +// operator = MathUnaryOperation.Operator.POSITIVE +// operand = parseNumericTermWithoutImplicitMultWithNumberOrUnaryPlusMinus() +// }.build() +// }.build() +// }*/ + + private fun parseNumericImplicitMultExpression2(): MathExpression { + // numeric_implicit_mult_expression = + // numeric_exp_expression , { numeric_term_without_unary_without_number } ; + var lastLhs = parseNumericExpExpression() + while (hasNextNumericTermWithoutUnary()) { + // Compute the next LHS if there is further implicit multiplication. + lastLhs = MathExpression.newBuilder().apply { binaryOperation = MathBinaryOperation.newBuilder().apply { operator = MathBinaryOperation.Operator.MULTIPLY - leftOperand = possibleLhs - rightOperand = parseImplicitlyMultipliableWithNumberTerm() + leftOperand = lastLhs + rightOperand = parseNumericTermWithoutUnaryWithoutNumber() }.build() }.build() - } else possibleLhs + } + return lastLhs + } + + private fun parseNumericExpExpression(): MathExpression { + // numeric_exp_expression = numeric_term_with_unary , [ numeric_exp_expression_tail ] ; + val possibleLhs = parseNumericTermWithUnary() + return when (tokens.peek()) { + is ExponentiationSymbol -> parseNumericExpExpressionTail(possibleLhs) + else -> possibleLhs + } + } + + // Use tail recursion so that the last exponentiation is evaluated first, and right-to-left + // associativity can be kept via backtracking. + private fun parseNumericExpExpressionTail(lhs: MathExpression): MathExpression { + // numeric_exp_expression_tail = exponentiation_operator , numeric_exp_expression ; + consumeTokenOfType { ExponentiationSymbol } + return MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = MathBinaryOperation.Operator.EXPONENTIATE + leftOperand = lhs + rightOperand = parseNumericExpExpression() + }.build() + }.build() + } + + private fun parseNumericTermWithUnary(): MathExpression { + // numeric_term_with_unary = + // number | numeric_term_without_unary_without_number | numeric_plus_minus_unary_term ; + return when (tokens.peek()) { + is MinusSymbol, is PlusSymbol -> parseNumericPlusMinusUnaryTerm() + is PositiveInteger, is PositiveRealNumber -> parseNumber() + else -> parseNumericTermWithoutUnaryWithoutNumber() + } + } + + private fun parseNumericTermWithoutUnaryWithoutNumber(): MathExpression { + // numeric_term_without_unary_without_number = + // numeric_function_expression | numeric_group_expression | numeric_rooted_term ; + return when (tokens.peek()) { + is FunctionName -> parseNumericFunctionExpression() + is LeftParenthesisSymbol -> parseNumericGroupExpression() + is SquareRootSymbol -> parseNumericRootedTerm() + else -> throw ParseException() + } + } + + private fun hasNextNumericTermWithoutUnary(): Boolean = when (tokens.peek()) { + is PositiveInteger, is PositiveRealNumber, is FunctionName, is LeftParenthesisSymbol, + is SquareRootSymbol -> true + else -> false + } + + private fun parseNumber(): MathExpression { + // number = positive_real_number | positive_integer ; + return MathExpression.newBuilder().apply { + constant = when ( + val numberToken = consumeNextTokenMatching { + it is PositiveInteger || it is PositiveRealNumber + } + ) { + is PositiveInteger -> Real.newBuilder().apply { + integer = numberToken.parsedValue + }.build() + is PositiveRealNumber -> Real.newBuilder().apply { + irrational = numberToken.parsedValue + }.build() + + // TODO: add error that one of the above was expected. Other error handling should maybe + // happen in the same way. + else -> throw ParseException() // Something went wrong. + } + }.build() + } + + private fun parseNumericFunctionExpression(): MathExpression { + // numeric_function_expression = function_name , left_paren , numeric_expression , right_paren ; + return MathExpression.newBuilder().apply { + val functionName = expectNextTokenWithType() + if (functionName.parsedName != "sqrt") throw ParseException() + consumeTokenOfType { LeftParenthesisSymbol } + functionCall = MathFunctionCall.newBuilder().apply { + functionType = MathFunctionCall.FunctionType.SQUARE_ROOT + argument = parseNumericExpression() + }.build() + consumeTokenOfType { RightParenthesisSymbol } + }.build() } private fun parseNumericGroupExpression(): MathExpression { @@ -282,55 +735,42 @@ class NumericExpressionParser(private val rawExpression: String) { private fun parseNumericPlusMinusUnaryTerm(): MathExpression { // numeric_plus_minus_unary_term = numeric_negated_term | numeric_positive_term ; - return if (tokens.peek() is MinusSymbol) { - parseNumericNegatedTerm() - } else parseNumericPositiveTerm() + return when (tokens.peek()) { + is MinusSymbol -> parseNumericNegatedTerm() + is PlusSymbol -> parseNumericPositiveTerm() + else -> throw ParseException() + } } private fun parseNumericNegatedTerm(): MathExpression { - // numeric_negated_term = minus_operator , numeric_term ; + // numeric_negated_term = minus_operator , numeric_expression ; consumeTokenOfType { MinusSymbol } return MathExpression.newBuilder().apply { unaryOperation = MathUnaryOperation.newBuilder().apply { operator = MathUnaryOperation.Operator.NEGATE - operand = parseNumericTerm() + operand = parseNumericMultDivExpression() }.build() }.build() } private fun parseNumericPositiveTerm(): MathExpression { - // numeric_positive_term = plus_operator , numeric_term ; + // numeric_positive_term = plus_operator , numeric_expression ; consumeTokenOfType { PlusSymbol } return MathExpression.newBuilder().apply { unaryOperation = MathUnaryOperation.newBuilder().apply { operator = MathUnaryOperation.Operator.POSITIVE - operand = parseNumericTerm() + operand = parseNumericMultDivExpression() }.build() }.build() } - private fun parseNumericRootedTermWithImplicitMultiplication(): MathExpression { - // numeric_rooted_term_with_implicit_multiplication = - // numeric_rooted_term , [ implicitly_multipliable_with_number_term ] ; - val possibleLhs = parseNumericRootedTerm() - return if (hasNextImplicitlyMultipliableWithNumberTerm()) { - MathExpression.newBuilder().apply { - binaryOperation = MathBinaryOperation.newBuilder().apply { - operator = MathBinaryOperation.Operator.MULTIPLY - leftOperand = possibleLhs - rightOperand = parseImplicitlyMultipliableWithNumberTerm() - }.build() - }.build() - } else possibleLhs - } - private fun parseNumericRootedTerm(): MathExpression { - // numeric_rooted_term = square_root_operator , numeric_term ; + // numeric_rooted_term = square_root_operator , numeric_term_with_unary ; consumeTokenOfType { SquareRootSymbol } return MathExpression.newBuilder().apply { functionCall = MathFunctionCall.newBuilder().apply { functionType = MathFunctionCall.FunctionType.SQUARE_ROOT - argument = parseNumericTerm() + argument = parseNumericTermWithUnary() }.build() }.build() } @@ -350,4 +790,17 @@ class NumericExpressionParser(private val rawExpression: String) { // TODO: do error handling better than this (& in a way that works better with the types of errors // that we want to show users). class ParseException : Exception() + + private data class MultiplicationOperands( + val first: MathExpression, + val second: MathExpression + ) { + fun toStandaloneExpression(): MathExpression = MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = MathBinaryOperation.Operator.MULTIPLY + leftOperand = first + rightOperand = second + }.build() + }.build() + } } diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 1fbbd952a47..1342042dae7 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -196,6 +196,33 @@ class NumericExpressionParserTest { expectFailureWhenParsing("73 2") + // Verify order of operations between higher & lower precedent operators. + val expression32 = parseExpression("3+4^7") + assertThat(expression32).hasStructureThatMatches { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(7) + } + } + } + } + } + } + assertThat(expression32).evaluatesToIntegerThat().isEqualTo(16387) + val expression7 = parseExpression("3*2-3+4^7*8/3*2+7") assertThat(expression7).hasStructureThatMatches { // To better visualize the precedence & order of operations, see this grouped version: @@ -335,31 +362,8 @@ class NumericExpressionParserTest { } assertThat(expression8).evaluatesToIntegerThat().isEqualTo(21) - val expression9 = parseExpression("(1+2)2") - assertThat(expression9).hasStructureThatMatches { - multiplication { - leftOperand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) - } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) - } - } - } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) - } - } - } - } - assertThat(expression9).evaluatesToIntegerThat().isEqualTo(6) + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsing("(1+2)2") val expression10 = parseExpression("2(1+2)") assertThat(expression10).hasStructureThatMatches { @@ -387,26 +391,8 @@ class NumericExpressionParserTest { } assertThat(expression10).evaluatesToIntegerThat().isEqualTo(6) - val expression11 = parseExpression("sqrt(2)3") - assertThat(expression11).hasStructureThatMatches { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withIntegerValueThat().isEqualTo(2) - } - } - } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(3) - } - } - } - } - assertThat(expression11).evaluatesToIrrationalThat().isWithin(1e-5).of(3.0 * sqrt(2.0)) + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsing("sqrt(2)3") val expression12 = parseExpression("3sqrt(2)") assertThat(expression12).hasStructureThatMatches { @@ -431,7 +417,6 @@ class NumericExpressionParserTest { expectFailureWhenParsing("xsqrt(2)") - // TODO: add version with implicit multiplication (has wrong associativity today). val expression13 = parseExpression("sqrt(2)*(1+2)*(3-2^5)") assertThat(expression13).hasStructureThatMatches { multiplication { @@ -489,6 +474,63 @@ class NumericExpressionParserTest { } assertThat(expression13).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) + val expression58 = parseExpression("sqrt(2)(1+2)(3-2^5)") + assertThat(expression58).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(5) + } + } + } + } + } + } + } + } + assertThat(expression58).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) + val expression14 = parseExpression("((3))") assertThat(expression14).hasStructureThatMatches { constant { @@ -627,24 +669,16 @@ class NumericExpressionParserTest { expectFailureWhenParsing("1+2 asdf") val expression21 = parseExpression("sqrt(2)sqrt(3)sqrt(4)") + // Note that this tree demonstrates left associativity. assertThat(expression21).hasStructureThatMatches { multiplication { leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withIntegerValueThat().isEqualTo(2) - } - } - } - } - rightOperand { multiplication { leftOperand { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(3) + withIntegerValueThat().isEqualTo(2) } } } @@ -653,13 +687,22 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(4) + withIntegerValueThat().isEqualTo(3) } } } } } } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } } } assertThat(expression21) @@ -671,22 +714,24 @@ class NumericExpressionParserTest { assertThat(expression22).hasStructureThatMatches { multiplication { leftOperand { - addition { + multiplication { leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + // 1+2 + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) - } - } - } - } - rightOperand { - multiplication { - leftOperand { + // 3-7^2 subtraction { leftOperand { constant { @@ -709,20 +754,21 @@ class NumericExpressionParserTest { } } } + } + } + rightOperand { + // 5+-17 + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(5) + } + } rightOperand { - addition { - leftOperand { + negation { + operand { constant { - withIntegerValueThat().isEqualTo(5) - } - } - rightOperand { - negation { - operand { - constant { - withIntegerValueThat().isEqualTo(17) - } - } + withIntegerValueThat().isEqualTo(17) } } } @@ -834,7 +880,6 @@ class NumericExpressionParserTest { // "Hard" order of operation problems loosely based on & other problems that can often stump // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. - // TODO: test implicit version (currently broken in parser) val expression29 = parseExpression("3÷2*(3+4)") assertThat(expression29).hasStructureThatMatches { multiplication { @@ -870,7 +915,823 @@ class NumericExpressionParserTest { } assertThat(expression29).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) - // TODO: add others + val expression59 = parseExpression("3÷2(3+4)") + assertThat(expression59).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression59).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) + + // Numbers cannot have implicit multiplication unless they are in groups. + expectFailureWhenParsing("2 2") + + expectFailureWhenParsing("2 2^2") + + expectFailureWhenParsing("2^2 2") + + val expression31 = parseExpression("(3)(4)(5)") + assertThat(expression31).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(5) + } + } + } + } + assertThat(expression31).evaluatesToIntegerThat().isEqualTo(60) + + val expression33 = parseExpression("2^(3)") + assertThat(expression33).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + assertThat(expression33).evaluatesToIntegerThat().isEqualTo(8) + + // Verify that implicit multiple has lower precedence than exponentiation. + val expression34 = parseExpression("2^(3)(4)") + assertThat(expression34).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + assertThat(expression34).evaluatesToIntegerThat().isEqualTo(32) + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsing("2^(3)2^2") + + val expression35 = parseExpression("2^(3)*2^2") + assertThat(expression35).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression35).evaluatesToIntegerThat().isEqualTo(32) + + // An exponentiation can be a right operand of an implicit mult if it's grouped. + val expression36 = parseExpression("2^(3)(2^2)") + assertThat(expression36).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression36).evaluatesToIntegerThat().isEqualTo(32) + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsing("2^3(4)2^3") + + val expression38 = parseExpression("2^3(4)*2^3") + assertThat(expression38).hasStructureThatMatches { + // 2^3(4)*2^3 + multiplication { + leftOperand { + // 2^3(4) + multiplication { + leftOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 4 + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + rightOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + } + } + assertThat(expression38).evaluatesToIntegerThat().isEqualTo(256) + + expectFailureWhenParsing("2^2 2^2") + expectFailureWhenParsing("(3) 2^2") + expectFailureWhenParsing("sqrt(3) 2^2") + expectFailureWhenParsing("√2 2^2") + expectFailureWhenParsing("2^2 3") + + expectFailureWhenParsing("-2 3") + + val expression39 = parseExpression("-(1+2)") + assertThat(expression39).hasStructureThatMatches { + negation { + operand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression39).evaluatesToIntegerThat().isEqualTo(-3) + + // Should pass for algebra. + expectFailureWhenParsing("-2 x") + + val expression40 = parseExpression("-2 (1+2)") + assertThat(expression40).hasStructureThatMatches { + // The negation happens last for parity with other common calculators. + negation { + operand { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + } + } + assertThat(expression40).evaluatesToIntegerThat().isEqualTo(-6) + + val expression41 = parseExpression("-2^3(4)") + assertThat(expression41).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression41).evaluatesToIntegerThat().isEqualTo(-32) + + val expression43 = parseExpression("√2^2(3)") + assertThat(expression43).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + assertThat(expression43).evaluatesToIrrationalThat().isWithin(1e-5).of(6.0) + + val expression60 = parseExpression("√(2^2(3))") + assertThat(expression60).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + } + } + assertThat(expression60).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(12.0)) + + val expression42 = parseExpression("-2*-2") + // Note that the following structure is not the same as (-2)*(-2) since unary negation has + // higher precedence than multiplication, so it's first & recurses to include the entire + // multiplication expression. + assertThat(expression42).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + } + } + assertThat(expression42).evaluatesToIntegerThat().isEqualTo(4) + + val expression44 = parseExpression("2(2)") + assertThat(expression44).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + assertThat(expression44).evaluatesToIntegerThat().isEqualTo(4) + + val expression45 = parseExpression("2sqrt(2)") + assertThat(expression45).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression45).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) + + val expression46 = parseExpression("2√2") + assertThat(expression46).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression46).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) + + val expression47 = parseExpression("(2)(2)") + assertThat(expression47).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + assertThat(expression47).evaluatesToIntegerThat().isEqualTo(4) + + val expression48 = parseExpression("sqrt(2)(2)") + assertThat(expression48).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + assertThat(expression48).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) + + val expression49 = parseExpression("sqrt(2)sqrt(2)") + assertThat(expression49).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression49).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) + + val expression50 = parseExpression("√2√2") + assertThat(expression50).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression50).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) + + val expression51 = parseExpression("(2)(2)(2)") + assertThat(expression51).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + assertThat(expression51).evaluatesToIntegerThat().isEqualTo(8) + + val expression52 = parseExpression("sqrt(2)sqrt(2)sqrt(2)") + assertThat(expression52).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + val sqrt2 = sqrt(2.0) + assertThat(expression52).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) + + val expression53 = parseExpression("√2√2√2") + assertThat(expression53).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression53).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) + + // Should fail for algebra. + expectFailureWhenParsing("x7") + + // Should pass for algebra. + expectFailureWhenParsing("2x^2") + + val expression54 = parseExpression("2*2/-4+7*2") + assertThat(expression54).hasStructureThatMatches { + // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) + addition { + leftOperand { + // 2*2/-4 + division { + leftOperand { + // 2*2 + multiplication { + leftOperand { + // 2 + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + // 2 + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + // -4 + negation { + // 4 + operand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + rightOperand { + // 7*2 + multiplication { + leftOperand { + // 7 + constant { + withIntegerValueThat().isEqualTo(7) + } + } + rightOperand { + // 2 + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression54).evaluatesToIntegerThat().isEqualTo(13) + + val expression55 = parseExpression("(3/(1-2))") + assertThat(expression55).hasStructureThatMatches { + division { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression55).evaluatesToIntegerThat().isEqualTo(-3) + + val expression56 = parseExpression("(3)/(1-2)") + assertThat(expression56).hasStructureThatMatches { + division { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression56).evaluatesToIntegerThat().isEqualTo(-3) + + val expression57 = parseExpression("3/((1-2))") + assertThat(expression57).hasStructureThatMatches { + division { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression57).evaluatesToIntegerThat().isEqualTo(-3) + + // TODO: add others, including tests for malformed expressions throughout the parser & + // tokenizer. } @DslMarker From ebc8c970a5ec631c681d961f719973d012072ddd Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 9 Nov 2021 21:20:38 -0800 Subject: [PATCH 073/289] Clean up old, unused parser code. --- .../util/math/NumericExpressionParser.kt | 545 +----------------- 1 file changed, 8 insertions(+), 537 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt index 418c2328a03..4004607542f 100644 --- a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt @@ -85,12 +85,16 @@ class NumericExpressionParser(private val rawExpression: String) { var lastLhs = parseNumericExpExpression() while (hasNextNumericMultDivExpressionRhs()) { // numeric_mult_div_expression_rhs = - // numeric_mult_expression_rhs | numeric_div_expression_rhs ; + // numeric_mult_expression_rhs + // | numeric_div_expression_rhs + // | numeric_implicit_mult_expression_rhs ; val (operator, rhs) = when (tokens.peek()) { is MultiplySymbol -> MathBinaryOperation.Operator.MULTIPLY to parseNumericMultExpressionRhs() is DivideSymbol -> MathBinaryOperation.Operator.DIVIDE to parseNumericDivExpressionRhs() - else -> MathBinaryOperation.Operator.MULTIPLY to parseNumericImplicitMultExpressionRhs() + // numeric_implicit_mult_expression_rhs = + // numeric_term_without_unary_without_number ; + else -> MathBinaryOperation.Operator.MULTIPLY to parseNumericTermWithoutUnaryWithoutNumber() } // Compute the next LHS if there is further multiplication/division. @@ -125,520 +129,6 @@ class NumericExpressionParser(private val rawExpression: String) { return parseNumericExpExpression() } - private fun parseNumericImplicitMultExpressionRhs(): MathExpression { - return parseNumericTermWithoutUnaryWithoutNumber() - } - - /*private fun parseNumericImplicitMultExpression(): MathExpression { - // numeric_implicit_mult_expression = - // numeric_exp_expression | numeric_term_implicit_mult_expression ; - // TODO: fix - val possibleLhs = parseNumericTermWithoutImplicitMultWithNumberWithUnaryPlusMinus() - return when { - tokens.peek() is ExponentiationSymbol -> - parseNumericExpExpression(numericTermWithoutImplicitMultWithNumberWithUnary = possibleLhs) - hasNextNumericTermImplicitMultExpression() -> - parseNumericTermImplicitMultExpression( - numericTermWithoutImplicitMultWithNumberOrUnaryPlusMinus = possibleLhs - ) - else -> possibleLhs // Nothing follows the term in this production rule. - } - } - - private fun parseNumericExpExpression( - numericTermWithoutImplicitMultWithNumberWithUnary: MathExpression - ): MathExpression { - // numeric_exp_expression = - // numeric_term_without_implicit_mult_with_number_with_unary_plus_minus - // , [ numeric_exp_expression_tail ] ; - @Suppress("UnnecessaryVariable") // The variable adds extra context for readability. - val possibleLhs = numericTermWithoutImplicitMultWithNumberWithUnary - return if (tokens.peek() is ExponentiationSymbol) { - parseNumericExpExpressionTail(possibleLhs) - } else possibleLhs - } - - // Use tail recursion so that the last exponentiation is evaluated first, and right-to-left - // associativity can be kept via backtracking. - private fun parseNumericExpExpressionTail(lhs: MathExpression): MathExpression { - // numeric_exp_expression_tail = - // exponentiation_operator , numeric_exp_expression ; - consumeTokenOfType { ExponentiationSymbol } - return MathExpression.newBuilder().apply { - binaryOperation = MathBinaryOperation.newBuilder().apply { - operator = MathBinaryOperation.Operator.EXPONENTIATE - leftOperand = lhs - rightOperand = - parseNumericExpExpression( - parseNumericTermWithoutImplicitMultWithNumberWithUnaryPlusMinus() - ) - }.build() - }.build() - } - - private fun parseNumericTermImplicitMultExpression(): MathExpression { - // numeric_term_implicit_mult_expression = - // numeric_term_implicit_mult_left_number_expansion_expression - // | numeric_term_implicit_mult_right_number_expansion_expression ; - - // TODO: verify associativity and maybe flip it since the recursion probably will result in - // right associativity. - return when { - hasNextNumber() -> parseNumericTermImplicitMultLeftNumberExpansionExpression() - else -> parseNumericTermImplicitMultRightNumberExpansionExpression() - } - -// var lastLhs = parseNumericTermImplicitMultInitialSubexpression().toStandaloneExpression() -// while (hasNextNumericTermImplicitMultLaterSubexpression()) { -// val operands = parseNumericTermWithoutImplicitMultWithoutNumberOrUnaryPlusMinus() -// } -// do { - // Compute the next LHS if there is further implicit multiplication. -// lastLhs = MathExpression.newBuilder().apply { -// binaryOperation = MathBinaryOperation.newBuilder().apply { -// operator = MathBinaryOperation.Operator.MULTIPLY -// leftOperand = lastLhs -// rightOperand = rhs -// }.build() -// }.build() -// } while (hasNextNumericTermWithoutImplicitMultWithoutNumber()) -// return lastLhs - } - - private fun parseNumericTermImplicitMultLeftNumberExpansionExpression(): MathExpression { - // numeric_term_implicit_mult_left_number_expansion_expression = - // numeric_term_implicit_mult_expression_with_number_initial_lhs - // , numeric_term_implicit_mult_without_number_expression_tail ; - // TODO: consider consolidating this with below. - return MathExpression.newBuilder().apply { - binaryOperation = MathBinaryOperation.newBuilder().apply { - operator = MathBinaryOperation.Operator.MULTIPLY - leftOperand = parseNumericTermImplicitMultExpressionWithNumberInitialLhs() - rightOperand = parseNumericTermImplicitMultWithoutNumberExpressionTail() - }.build() - }.build() - } - - private fun parseNumericTermImplicitMultRightNumberExpansionExpression(): MathExpression { - // numeric_term_implicit_mult_right_number_expansion_expression = - // numeric_term_implicit_mult_expression_without_number_initial_lhs - // , numeric_term_implicit_mult_with_number_expression_tail ; - return MathExpression.newBuilder().apply { - binaryOperation = MathBinaryOperation.newBuilder().apply { - operator = MathBinaryOperation.Operator.MULTIPLY - leftOperand = parseNumericTermImplicitMultExpressionWithoutNumberInitialLhs() - rightOperand = parseNumericTermImplicitMultWithNumberExpressionTail() - }.build() - }.build() - } - -// private fun parseNumericTermImplicitMultInitialSubexpression(): MultiplicationOperands { - // numeric_term_implicit_mult_initial_subexpression = - // numeric_term_implicit_mult_initial_left_number_subexpression - // | numeric_term_implicit_mult_initial_right_number_subexpression ; -// return if (hasNextNumber()) { -// parseNumericTermImplicitMultInitialLeftNumberSubexpression() -// } else parseNumericTermImplicitMultInitialRightNumberSubexpression() -// } - -// private fun parseNumericTermImplicitMultInitialLeftNumberSubexpression(): MultiplicationOperands { - // numeric_term_implicit_mult_initial_left_number_subexpression = - // numeric_term_implicit_mult_expression_with_number_initial_lhs - // , numeric_term_implicit_mult_expression_without_number_rhs ; -// return MultiplicationOperands( -// first = parseNumericTermImplicitMultExpressionWithNumberInitialLhs(), -// second = parseNumericTermImplicitMultExpressionWithoutNumberRhs() -// ) -// } - -// private fun parseNumericTermImplicitMultInitialRightNumberSubexpression(): MultiplicationOperands { - // numeric_term_implicit_mult_initial_right_number_subexpression = - // numeric_term_implicit_mult_expression_without_number_initial_lhs - // , numeric_term_implicit_mult_expression_with_number_rhs ; -// return MultiplicationOperands( -// first = parseNumericTermImplicitMultExpressionWithoutNumberInitialLhs(), -// second = parseNumericTermImplicitMultExpressionWithNumberRhs() -// ) -// } - -// private fun parseNumericTermImplicitMultLaterSubexpression(): MultiplicationOperands { - // numeric_term_implicit_mult_later_subexpression = - // numeric_term_implicit_mult_later_left_number_subexpression - // | numeric_term_implicit_mult_later_right_number_subexpression ; -// return if (hasNextNumber()) { -// parseNumericTermImplicitMultLaterLeftNumberSubexpression() -// } else parseNumericTermImplicitMultLaterRightNumberSubexpression() -// } - -// private fun parseNumericTermImplicitMultLaterLeftNumberSubexpression(): MultiplicationOperands { - // numeric_term_implicit_mult_later_left_number_subexpression = - // numeric_term_implicit_mult_expression_with_number_later_lhs - // , numeric_term_implicit_mult_expression_without_number_rhs ; -// return MultiplicationOperands( -// first = parseNumericTermImplicitMultExpressionWithNumberLaterLhs(), -// second = parseNumericTermImplicitMultExpressionWithoutNumberRhs() -// ) -// } - - // TODO: consider consolidating this with the other implicit multiplication cases. -// private fun parseNumericTermImplicitMultLaterRightNumberSubexpression(): MultiplicationOperands { - // numeric_term_implicit_mult_later_right_number_subexpression = - // numeric_term_implicit_mult_expression_without_number_later_lhs - // , numeric_term_implicit_mult_expression_with_number_rhs ; -// return MultiplicationOperands( -// first = parseNumericTermImplicitMultExpressionWithoutNumberLaterLhs(), -// second = parseNumericTermImplicitMultExpressionWithNumberRhs() -// ) -// } - - private fun parseNumericTermImplicitMultWithoutNumberExpressionTail(): MathExpression { - // numeric_term_implicit_mult_without_number_expression_tail = - // numeric_term_implicit_mult_expression_without_number_rhs ; - return parseNumericTermImplicitMultExpressionWithoutNumberRhs() - } - - private fun parseNumericTermImplicitMultWithNumberExpressionTail(): MathExpression { - // numeric_term_implicit_mult_with_number_expression_tail = - // numeric_term_implicit_mult_expression_with_number_rhs ; - return parseNumericTermImplicitMultExpressionWithNumberRhs() - } - - // TODO: consider inlining these for simplicity. - private fun parseNumericTermImplicitMultExpressionWithNumberInitialLhs(): MathExpression { - // numeric_term_implicit_mult_expression_with_number_initial_lhs = - // numeric_term_without_implicit_mult_with_number_with_unary_plus_minus ; - return parseNumericTermWithoutImplicitMultWithNumberWithUnaryPlusMinus() - } - - private fun parseNumericTermImplicitMultExpressionWithoutNumberInitialLhs(): MathExpression { - // numeric_term_implicit_mult_expression_without_number_initial_lhs = - // numeric_term_without_implicit_mult_no_number_with_unary_plus_minus ; - return parseNumericTermWithoutImplicitMultNoNumberWithUnaryPlusMinus() - } - - private fun parseNumericTermImplicitMultExpressionWithNumberLaterLhs(): MathExpression { - // numeric_term_implicit_mult_expression_with_number_later_lhs = - // number | numeric_term_implicit_mult_expression_without_number_later_lhs ; - return when { - hasNextNumber() -> parseNumber() - else -> parseNumericTermImplicitMultExpressionWithoutNumberLaterLhs() - } - } - - private fun parseNumericTermImplicitMultExpressionWithoutNumberLaterLhs(): MathExpression { - // numeric_term_implicit_mult_expression_without_number_later_lhs = - // numeric_term_without_implicit_mult_no_number_no_unary_plus_minus ; - return parseNumericTermWithoutImplicitMultNoNumberNoUnaryPlusMinus() - } - - private fun parseNumericTermImplicitMultExpressionWithNumberRhs(): MathExpression { - // numeric_term_implicit_mult_expression_with_number_rhs = - // ( number | numeric_term_without_implicit_mult_no_number_no_unary_plus_minus ) - // , [ numeric_term_implicit_mult_without_number_expression_tail ] ; - val possibleLhs = when { - hasNextNumber() -> parseNumber() - else -> parseNumericTermImplicitMultExpressionWithoutNumberRhs() - } - // TODO: consider consolidating this with the other rhs method. - return if (hasNextNumericTermImplicitMultWithoutNumberExpressionTail()) { - MathExpression.newBuilder().apply { - binaryOperation = MathBinaryOperation.newBuilder().apply { - operator = MathBinaryOperation.Operator.MULTIPLY - leftOperand = possibleLhs - rightOperand = parseNumericTermImplicitMultWithoutNumberExpressionTail() - }.build() - }.build() - } else possibleLhs - } - - private fun parseNumericTermImplicitMultExpressionWithoutNumberRhs(): MathExpression { - // numeric_term_implicit_mult_expression_without_number_rhs = - // numeric_term_without_implicit_mult_no_number_no_unary_plus_minus - // , [ numeric_term_implicit_mult_with_number_expression_tail ] ; - val possibleLhs = parseNumericTermWithoutImplicitMultNoNumberNoUnaryPlusMinus() - return if (hasNextNumericTermImplicitMultWithNumberExpressionTail()) { - MathExpression.newBuilder().apply { - binaryOperation = MathBinaryOperation.newBuilder().apply { - operator = MathBinaryOperation.Operator.MULTIPLY - leftOperand = possibleLhs - rightOperand = parseNumericTermImplicitMultWithNumberExpressionTail() - }.build() - }.build() - } else possibleLhs - } - - private fun parseNumericTermWithoutImplicitMultWithNumberWithUnaryPlusMinus(): MathExpression { - // numeric_term_without_implicit_mult_with_number_with_unary_plus_minus = - // number - // | numeric_term_without_implicit_mult_no_number_no_unary_plus_minus - // | numeric_plus_minus_unary_term_with_number ; - return when (tokens.peek()) { - is PositiveInteger, is PositiveRealNumber -> parseNumber() - is PlusSymbol, is MinusSymbol -> parseNumericPlusMinusUnaryTermWithNumber() - else -> parseNumericTermWithoutImplicitMultNoNumberNoUnaryPlusMinus() - } - } - - private fun parseNumericTermWithoutImplicitMultNoNumberWithUnaryPlusMinus(): MathExpression { - // numeric_term_without_implicit_mult_no_number_with_unary_plus_minus = - // numeric_term_without_implicit_mult_no_number_no_unary_plus_minus - // | numeric_plus_minus_unary_term_without_number ; - return when (tokens.peek()) { - is PlusSymbol, is MinusSymbol -> parseNumericPlusMinusUnaryTermWithoutNumber() - else -> parseNumericTermWithoutImplicitMultNoNumberNoUnaryPlusMinus() - } - } - - private fun parseNumericTermWithoutImplicitMultNoNumberNoUnaryPlusMinus(): MathExpression { - // numeric_term_without_implicit_mult_no_number_no_unary_plus_minus = - // numeric_function_expression - // | numeric_group_expression - // | numeric_rooted_term ; - return when (tokens.peek()) { - is FunctionName -> parseNumericFunctionExpression() - is LeftParenthesisSymbol -> parseNumericGroupExpression() - is SquareRootSymbol -> parseNumericRootedTerm() - else -> throw ParseException() - } - } - -// private fun parseNumericTermWithImplicitMult( -// numericTermWithoutImplicitMultWithNumberOrUnaryPlusMinus: MathExpression -// ): MathExpression { - // numeric_term_with_implicit_mult = - // numeric_term_implicit_mult_expression_lhs - // , { numeric_term_implicit_mult_expression_rhs }- ; - // numeric_term_implicit_mult_expression_lhs = - // numeric_term_without_implicit_mult_with_number_or_unary_plus_minus -// var lastLhs = numericTermWithoutImplicitMultWithNumberOrUnaryPlusMinus -// do { - // numeric_term_implicit_mult_expression_rhs = - // numeric_term_without_implicit_mult_without_number_or_unary_plus_minus -// val rhs = parseNumericTermWithoutImplicitMultWithoutNumberOrUnaryPlusMinus() -// - // Compute the next LHS if there is further implicit multiplication. -// lastLhs = MathExpression.newBuilder().apply { -// binaryOperation = MathBinaryOperation.newBuilder().apply { -// operator = MathBinaryOperation.Operator.MULTIPLY -// leftOperand = lastLhs -// rightOperand = rhs -// }.build() -// }.build() -// } while (hasNextNumericTermWithoutImplicitMultWithoutNumber()) -// return lastLhs -// } - -// private fun parseNumericTermWithoutImplicitMultWithNumberOrUnaryPlusMinus(): MathExpression { - // numeric_term_without_implicit_mult_with_number_or_unary_plus_minus = - // number - // | numeric_term_without_implicit_mult_without_number - // | numeric_plus_minus_unary_term; -// return when (tokens.peek()) { -// is PositiveInteger, is PositiveRealNumber -> parseNumber() -// is PlusSymbol, is MinusSymbol -> parseNumericPlusMinusUnaryTerm() -// else -> parseNumericTermWithoutImplicitMultWithoutNumberOrUnaryPlusMinus() -// } -// } - -// private fun parseNumericTermWithoutImplicitMultWithoutNumberOrUnaryPlusMinus(): MathExpression { - // numeric_term_without_implicit_mult_without_number_or_unary_plus_minus = - // numeric_function_expression - // | numeric_group_expression - // | numeric_rooted_term; -// return when (tokens.peek()) { -// is FunctionName -> parseNumericFunctionExpression() -// is LeftParenthesisSymbol -> parseNumericGroupExpression() -// is SquareRootSymbol -> parseNumericRootedTerm() -// else -> throw ParseException() -// } -// } - -// private fun hasNextNumericTermWithoutImplicitMultWithoutNumber(): Boolean = when (tokens.peek()) { -// is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> true -// else -> false -// } - - private fun parseNumber(): MathExpression { - // number = positive_real_number | positive_integer ; - return MathExpression.newBuilder().apply { - constant = when ( - val numberToken = consumeNextTokenMatching { - it is PositiveInteger || it is PositiveRealNumber - } - ) { - is PositiveInteger -> Real.newBuilder().apply { - integer = numberToken.parsedValue - }.build() - is PositiveRealNumber -> Real.newBuilder().apply { - irrational = numberToken.parsedValue - }.build() - - // TODO: add error that one of the above was expected. Other error handling should maybe - // happen in the same way. - else -> throw ParseException() // Something went wrong. - } - }.build() - } - - private fun hasNextNumber(): Boolean = when (tokens.peek()) { - is PositiveInteger, is PositiveRealNumber -> true - else -> false - } - - private fun parseNumericFunctionExpression(): MathExpression { - // numeric_function_expression = - // function_name , left_paren , numeric_expression , right_paren ; - return MathExpression.newBuilder().apply { - val functionName = expectNextTokenWithType() - if (functionName.parsedName != "sqrt") throw ParseException() - consumeTokenOfType { LeftParenthesisSymbol } - functionCall = MathFunctionCall.newBuilder().apply { - functionType = MathFunctionCall.FunctionType.SQUARE_ROOT - argument = parseNumericExpression() - }.build() - consumeTokenOfType { RightParenthesisSymbol } - }.build() - } - - private fun parseNumericGroupExpression(): MathExpression { - // numeric_group_expression = left_paren , numeric_expression , right_paren ; - consumeTokenOfType { LeftParenthesisSymbol } - return parseNumericExpression().also { - consumeTokenOfType { RightParenthesisSymbol } - } - } - - private fun parseNumericRootedTerm(): MathExpression { - // numeric_rooted_term = - // square_root_operator - // , numeric_term_without_implicit_mult_with_number_with_unary_plus_minus ; - consumeTokenOfType { SquareRootSymbol } - return MathExpression.newBuilder().apply { - functionCall = MathFunctionCall.newBuilder().apply { - functionType = MathFunctionCall.FunctionType.SQUARE_ROOT - argument = parseNumericTermWithoutImplicitMultWithNumberWithUnaryPlusMinus() - }.build() - }.build() - } - - private fun parseNumericPlusMinusUnaryTermWithNumber(): MathExpression { - // numeric_plus_minus_unary_term_with_number = - // numeric_negated_term_with_number | numeric_positive_term_with_number ; - return if (tokens.peek() is MinusSymbol) { - parseNumericNegatedTermWithNumber() - } else parseNumericPositiveTermWithNumber() - } - - private fun parseNumericPlusMinusUnaryTermWithoutNumber(): MathExpression { - // numeric_plus_minus_unary_term_without_number = - // numeric_negated_term_without_number - // | numeric_positive_term_without_number ; - return if (tokens.peek() is MinusSymbol) { - parseNumericNegatedTermWithoutNumber() - } else parseNumericPositiveTermWithoutNumber() - } - - // TODO: consider consolidating the similar negated/positive methods. - private fun parseNumericNegatedTermWithNumber(): MathExpression { - // numeric_negated_term_with_number = - // minus_operator - // , numeric_term_without_implicit_mult_with_number_with_unary_plus_minus ; - consumeTokenOfType { MinusSymbol } - return MathExpression.newBuilder().apply { - unaryOperation = MathUnaryOperation.newBuilder().apply { - operator = MathUnaryOperation.Operator.NEGATE - operand = parseNumericTermWithoutImplicitMultWithNumberWithUnaryPlusMinus() - }.build() - }.build() - } - - private fun parseNumericNegatedTermWithoutNumber(): MathExpression { - // numeric_negated_term_without_number = - // minus_operator - // , numeric_term_without_implicit_mult_no_number_no_unary_plus_minus ; - consumeTokenOfType { MinusSymbol } - return MathExpression.newBuilder().apply { - unaryOperation = MathUnaryOperation.newBuilder().apply { - operator = MathUnaryOperation.Operator.NEGATE - operand = parseNumericTermWithoutImplicitMultNoNumberNoUnaryPlusMinus() - }.build() - }.build() - } - - private fun parseNumericPositiveTermWithNumber(): MathExpression { - // numeric_positive_term_with_number = - // plus_operator - // , numeric_term_without_implicit_mult_with_number_with_unary_plus_minus ; - consumeTokenOfType { PlusSymbol } - return MathExpression.newBuilder().apply { - unaryOperation = MathUnaryOperation.newBuilder().apply { - operator = MathUnaryOperation.Operator.POSITIVE - operand = parseNumericTermWithoutImplicitMultWithNumberWithUnaryPlusMinus() - }.build() - }.build() - } - - private fun parseNumericPositiveTermWithoutNumber(): MathExpression { - // numeric_positive_term_without_number = - // plus_operator - // , numeric_term_without_implicit_mult_no_number_no_unary_plus_minus ; - consumeTokenOfType { PlusSymbol } - return MathExpression.newBuilder().apply { - unaryOperation = MathUnaryOperation.newBuilder().apply { - operator = MathUnaryOperation.Operator.POSITIVE - operand = parseNumericTermWithoutImplicitMultNoNumberNoUnaryPlusMinus() - }.build() - }.build() - } - -// private fun parseNumericPlusMinusUnaryTerm(): MathExpression { - // numeric_plus_minus_unary_term = numeric_negated_term | numeric_positive_term ; -// return if (tokens.peek() is MinusSymbol) { -// parseNumericNegatedTerm() -// } else parseNumericPositiveTerm() -// } - -// private fun parseNumericNegatedTerm(): MathExpression { - // numeric_negated_term = - // minus_operator , numeric_term_without_implicit_mult_with_number_or_unary_plus_minus ; -// consumeTokenOfType { MinusSymbol } -// return MathExpression.newBuilder().apply { -// unaryOperation = MathUnaryOperation.newBuilder().apply { -// operator = MathUnaryOperation.Operator.NEGATE -// operand = parseNumericTermWithoutImplicitMultWithNumberOrUnaryPlusMinus() -// }.build() -// }.build() -// } - -// private fun parseNumericPositiveTerm(): MathExpression { - // numeric_positive_term = - // plus_operator , numeric_term_without_implicit_mult_with_number_or_unary_plus_minus ; -// consumeTokenOfType { PlusSymbol } -// return MathExpression.newBuilder().apply { -// unaryOperation = MathUnaryOperation.newBuilder().apply { -// operator = MathUnaryOperation.Operator.POSITIVE -// operand = parseNumericTermWithoutImplicitMultWithNumberOrUnaryPlusMinus() -// }.build() -// }.build() -// }*/ - - private fun parseNumericImplicitMultExpression2(): MathExpression { - // numeric_implicit_mult_expression = - // numeric_exp_expression , { numeric_term_without_unary_without_number } ; - var lastLhs = parseNumericExpExpression() - while (hasNextNumericTermWithoutUnary()) { - // Compute the next LHS if there is further implicit multiplication. - lastLhs = MathExpression.newBuilder().apply { - binaryOperation = MathBinaryOperation.newBuilder().apply { - operator = MathBinaryOperation.Operator.MULTIPLY - leftOperand = lastLhs - rightOperand = parseNumericTermWithoutUnaryWithoutNumber() - }.build() - }.build() - } - return lastLhs - } - private fun parseNumericExpExpression(): MathExpression { // numeric_exp_expression = numeric_term_with_unary , [ numeric_exp_expression_tail ] ; val possibleLhs = parseNumericTermWithUnary() @@ -683,12 +173,6 @@ class NumericExpressionParser(private val rawExpression: String) { } } - private fun hasNextNumericTermWithoutUnary(): Boolean = when (tokens.peek()) { - is PositiveInteger, is PositiveRealNumber, is FunctionName, is LeftParenthesisSymbol, - is SquareRootSymbol -> true - else -> false - } - private fun parseNumber(): MathExpression { // number = positive_real_number | positive_integer ; return MathExpression.newBuilder().apply { @@ -743,7 +227,7 @@ class NumericExpressionParser(private val rawExpression: String) { } private fun parseNumericNegatedTerm(): MathExpression { - // numeric_negated_term = minus_operator , numeric_expression ; + // numeric_negated_term = minus_operator , numeric_mult_div_expression ; consumeTokenOfType { MinusSymbol } return MathExpression.newBuilder().apply { unaryOperation = MathUnaryOperation.newBuilder().apply { @@ -754,7 +238,7 @@ class NumericExpressionParser(private val rawExpression: String) { } private fun parseNumericPositiveTerm(): MathExpression { - // numeric_positive_term = plus_operator , numeric_expression ; + // numeric_positive_term = plus_operator , numeric_mult_div_expression ; consumeTokenOfType { PlusSymbol } return MathExpression.newBuilder().apply { unaryOperation = MathUnaryOperation.newBuilder().apply { @@ -790,17 +274,4 @@ class NumericExpressionParser(private val rawExpression: String) { // TODO: do error handling better than this (& in a way that works better with the types of errors // that we want to show users). class ParseException : Exception() - - private data class MultiplicationOperands( - val first: MathExpression, - val second: MathExpression - ) { - fun toStandaloneExpression(): MathExpression = MathExpression.newBuilder().apply { - binaryOperation = MathBinaryOperation.newBuilder().apply { - operator = MathBinaryOperation.Operator.MULTIPLY - leftOperand = first - rightOperand = second - }.build() - }.build() - } } From 91560773420c2b1ad7801ac18f29c2b281764965 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 11 Nov 2021 18:59:18 -0800 Subject: [PATCH 074/289] Generalize parser & add algebraic exp support. + Tests for algebraic expressions (mostly a repeat of the numeric versions; a better long-term strategy for test cases needs to be figured out since the test suite is going to become way too long otherwise). --- .../oppia/android/util/math/MathTokenizer2.kt | 1 + .../util/math/NumericExpressionParser.kt | 407 ++-- .../util/math/NumericExpressionParserTest.kt | 2059 ++++++++++++++++- 3 files changed, 2247 insertions(+), 220 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt index 8ce54ec55d0..8fefab29949 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt @@ -51,6 +51,7 @@ class MathTokenizer2 private constructor() { // Another integer must follow the ".". val integerPart2 = parseInteger(chars) ?: return Token.InvalidToken + // TODO: validate that the result isn't NaN or INF. val doubleValue = "$integerPart1.$integerPart2".toDoubleOrNull() ?: return Token.InvalidToken Token.PositiveRealNumber(doubleValue) diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt index 4004607542f..78ab93f422c 100644 --- a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt @@ -17,36 +17,49 @@ import org.oppia.android.util.math.MathTokenizer2.Companion.Token.PositiveIntege import org.oppia.android.util.math.MathTokenizer2.Companion.Token.PositiveRealNumber import org.oppia.android.util.math.MathTokenizer2.Companion.Token.RightParenthesisSymbol import org.oppia.android.util.math.MathTokenizer2.Companion.Token.SquareRootSymbol +import org.oppia.android.util.math.MathTokenizer2.Companion.Token.VariableName -class NumericExpressionParser(private val rawExpression: String) { +class NumericExpressionParser private constructor( + private val rawExpression: String, + private val parseContext: ParseContext +) { private val tokens: PeekableIterator by lazy { PeekableIterator.fromSequence(MathTokenizer2.tokenize(rawExpression)) } - fun parse(): MathExpression { - return parseNumericExpression().also { + // TODO: + // - Add helpers to reduce overall parser length. + // - Integrate with new errors & update the routines to not rely on exceptions except in actual exceptional cases. Make sure optional errors can be disabled (for testing purposes). + // - Rename this to be a generic parser, update the public API, add documentation, remove the old classes, and split up the big test routines into actual separate tests. + // - Add support for equations + tests. + + // TODO: document that 'generic' means either 'numeric' or 'algebraic' (ie that the expression is syntactically the same between both grammars). + + private fun parseGeneric(): MathExpression { + return parseGenericExpression().also { // Make sure all tokens were consumed (otherwise there are trailing tokens which invalidate // the whole expression). if (tokens.hasNext()) throw ParseException() } } - private fun parseNumericExpression(): MathExpression { - // numeric_expression = numeric_add_sub_expression ; - return parseNumericAddSubExpression() + private fun parseGenericExpression(): MathExpression { + // generic_expression = generic_add_sub_expression ; + return parseGenericAddSubExpression() } // TODO: consider consolidating this with other binary parsing to reduce the overall parser. - private fun parseNumericAddSubExpression(): MathExpression { - // numeric_add_sub_expression = - // numeric_mult_div_expression , { numeric_add_sub_expression_rhs } ; - var lastLhs = parseNumericMultDivExpression() - while (hasNextNumericAddSubExpressionRhs()) { - // numeric_add_sub_expression_rhs = - // numeric_add_expression_rhs | numeric_sub_expression_rhs ; - val (operator, rhs) = when (tokens.peek()) { - is PlusSymbol -> MathBinaryOperation.Operator.ADD to parseNumericAddExpressionRhs() - is MinusSymbol -> MathBinaryOperation.Operator.SUBTRACT to parseNumericSubExpressionRhs() + private fun parseGenericAddSubExpression(): MathExpression { + // generic_add_sub_expression = + // generic_mult_div_expression , { generic_add_sub_expression_rhs } ; + var lastLhs = parseGenericMultDivExpression() + while (hasNextGenericAddSubExpressionRhs()) { + // generic_add_sub_expression_rhs = generic_add_expression_rhs | generic_sub_expression_rhs ; + val (operator, rhs) = when { + hasNextGenericAddExpressionRhs() -> + MathBinaryOperation.Operator.ADD to parseGenericAddExpressionRhs() + hasNextGenericSubExpressionRhs() -> + MathBinaryOperation.Operator.SUBTRACT to parseGenericSubExpressionRhs() else -> throw ParseException() } @@ -62,39 +75,42 @@ class NumericExpressionParser(private val rawExpression: String) { return lastLhs } - private fun hasNextNumericAddSubExpressionRhs() = when (tokens.peek()) { - is PlusSymbol, is MinusSymbol -> true - else -> false - } + private fun hasNextGenericAddSubExpressionRhs() = hasNextGenericAddExpressionRhs() + || hasNextGenericSubExpressionRhs() - private fun parseNumericAddExpressionRhs(): MathExpression { - // numeric_add_expression_rhs = plus_operator , numeric_mult_div_expression ; - consumeTokenOfType { PlusSymbol } - return parseNumericMultDivExpression() + private fun hasNextGenericAddExpressionRhs(): Boolean = tokens.peek() is PlusSymbol + + private fun parseGenericAddExpressionRhs(): MathExpression { + // generic_add_expression_rhs = plus_operator , generic_mult_div_expression ; + consumeTokenOfType() + return parseGenericMultDivExpression() } - private fun parseNumericSubExpressionRhs(): MathExpression { - // numeric_sub_expression_rhs = minus_operator , numeric_mult_div_expression ; - consumeTokenOfType { MinusSymbol } - return parseNumericMultDivExpression() + private fun hasNextGenericSubExpressionRhs(): Boolean = tokens.peek() is MinusSymbol + + private fun parseGenericSubExpressionRhs(): MathExpression { + // generic_sub_expression_rhs = minus_operator , generic_mult_div_expression ; + consumeTokenOfType() + return parseGenericMultDivExpression() } - private fun parseNumericMultDivExpression(): MathExpression { - // numeric_mult_div_expression = - // numeric_implicit_mult_expression , { numeric_mult_div_expression_rhs } ; - var lastLhs = parseNumericExpExpression() - while (hasNextNumericMultDivExpressionRhs()) { - // numeric_mult_div_expression_rhs = - // numeric_mult_expression_rhs - // | numeric_div_expression_rhs - // | numeric_implicit_mult_expression_rhs ; - val (operator, rhs) = when (tokens.peek()) { - is MultiplySymbol -> - MathBinaryOperation.Operator.MULTIPLY to parseNumericMultExpressionRhs() - is DivideSymbol -> MathBinaryOperation.Operator.DIVIDE to parseNumericDivExpressionRhs() - // numeric_implicit_mult_expression_rhs = - // numeric_term_without_unary_without_number ; - else -> MathBinaryOperation.Operator.MULTIPLY to parseNumericTermWithoutUnaryWithoutNumber() + private fun parseGenericMultDivExpression(): MathExpression { + // generic_mult_div_expression = + // generic_exp_expression , { generic_mult_div_expression_rhs } ; + var lastLhs = parseGenericExpExpression() + while (hasNextGenericMultDivExpressionRhs()) { + // generic_mult_div_expression_rhs = + // generic_mult_expression_rhs + // | generic_div_expression_rhs + // | generic_implicit_mult_expression_rhs ; + val (operator, rhs) = when { + hasNextGenericMultExpressionRhs() -> + MathBinaryOperation.Operator.MULTIPLY to parseGenericMultExpressionRhs() + hasNextGenericDivExpressionRhs() -> + MathBinaryOperation.Operator.DIVIDE to parseGenericDivExpressionRhs() + hasNextGenericImplicitMultExpressionRhs() -> + MathBinaryOperation.Operator.MULTIPLY to parseGenericImplicitMultExpressionRhs() + else -> throw ParseException() } // Compute the next LHS if there is further multiplication/division. @@ -109,169 +125,296 @@ class NumericExpressionParser(private val rawExpression: String) { return lastLhs } - private fun hasNextNumericMultDivExpressionRhs() = when (tokens.peek()) { - is MultiplySymbol, is DivideSymbol, is FunctionName, is LeftParenthesisSymbol, - is SquareRootSymbol -> true - else -> false + private fun hasNextGenericMultDivExpressionRhs(): Boolean = + hasNextGenericMultExpressionRhs() + || hasNextGenericDivExpressionRhs() + || hasNextGenericImplicitMultExpressionRhs() + + private fun hasNextGenericMultExpressionRhs(): Boolean = tokens.peek() is MultiplySymbol + + private fun parseGenericMultExpressionRhs(): MathExpression { + // generic_mult_expression_rhs = multiplication_operator , generic_exp_expression ; + consumeTokenOfType() + return parseGenericExpExpression() } - private fun parseNumericMultExpressionRhs(): MathExpression { - // numeric_mult_expression_rhs = - // multiplication_operator , numeric_implicit_mult_expression ; - consumeTokenOfType { MultiplySymbol } - return parseNumericExpExpression() + private fun hasNextGenericDivExpressionRhs(): Boolean = tokens.peek() is DivideSymbol + + private fun parseGenericDivExpressionRhs(): MathExpression { + // generic_div_expression_rhs = division_operator , generic_exp_expression ; + consumeTokenOfType() + return parseGenericExpExpression() } - private fun parseNumericDivExpressionRhs(): MathExpression { - // numeric_div_expression_rhs = - // division_operator , numeric_implicit_mult_expression ; - consumeTokenOfType { DivideSymbol } - return parseNumericExpExpression() + private fun hasNextGenericImplicitMultExpressionRhs(): Boolean { + return when (parseContext) { + ParseContext.NumericExpressionContext -> hasNextNumericImplicitMultExpressionRhs() + is ParseContext.AlgebraicExpressionContext -> hasNextAlgebraicImplicitMultOrExpExpressionRhs() + } + } + + private fun parseGenericImplicitMultExpressionRhs(): MathExpression { + // generic_implicit_mult_expression_rhs is either numeric_implicit_mult_expression_rhs or + // algebraic_implicit_mult_or_exp_expression_rhs depending on the current parser context. + return when (parseContext) { + ParseContext.NumericExpressionContext -> parseNumericImplicitMultExpressionRhs() + is ParseContext.AlgebraicExpressionContext -> parseAlgebraicImplicitMultOrExpExpressionRhs() + } + } + + private fun hasNextNumericImplicitMultExpressionRhs(): Boolean = + hasNextGenericTermWithoutUnaryWithoutNumber() + + private fun parseNumericImplicitMultExpressionRhs(): MathExpression { + // numeric_implicit_mult_expression_rhs = generic_term_without_unary_without_number ; + return parseGenericTermWithoutUnaryWithoutNumber() + } + + private fun hasNextAlgebraicImplicitMultOrExpExpressionRhs(): Boolean = + hasNextGenericTermWithoutUnaryWithoutNumber() + + private fun parseAlgebraicImplicitMultOrExpExpressionRhs(): MathExpression { + // algebraic_implicit_mult_or_exp_expression_rhs = + // generic_term_without_unary_without_number , [ generic_exp_expression_tail ] ; + val possibleLhs = parseGenericTermWithoutUnaryWithoutNumber() + return if (tokens.peek() is ExponentiationSymbol) { + parseGenericExpExpressionTail(possibleLhs) + } else possibleLhs } - private fun parseNumericExpExpression(): MathExpression { - // numeric_exp_expression = numeric_term_with_unary , [ numeric_exp_expression_tail ] ; - val possibleLhs = parseNumericTermWithUnary() - return when (tokens.peek()) { - is ExponentiationSymbol -> parseNumericExpExpressionTail(possibleLhs) + private fun parseGenericExpExpression(): MathExpression { + // generic_exp_expression = generic_term_with_unary , [ generic_exp_expression_tail ] ; + val possibleLhs = parseGenericTermWithUnary() + return when { + hasNextGenericExpExpressionTail() -> parseGenericExpExpressionTail(possibleLhs) else -> possibleLhs } } + private fun hasNextGenericExpExpressionTail(): Boolean = tokens.peek() is ExponentiationSymbol + // Use tail recursion so that the last exponentiation is evaluated first, and right-to-left // associativity can be kept via backtracking. - private fun parseNumericExpExpressionTail(lhs: MathExpression): MathExpression { - // numeric_exp_expression_tail = exponentiation_operator , numeric_exp_expression ; - consumeTokenOfType { ExponentiationSymbol } + private fun parseGenericExpExpressionTail(lhs: MathExpression): MathExpression { + // generic_exp_expression_tail = exponentiation_operator , generic_exp_expression ; + consumeTokenOfType() return MathExpression.newBuilder().apply { binaryOperation = MathBinaryOperation.newBuilder().apply { operator = MathBinaryOperation.Operator.EXPONENTIATE leftOperand = lhs - rightOperand = parseNumericExpExpression() + rightOperand = parseGenericExpExpression() }.build() }.build() } - private fun parseNumericTermWithUnary(): MathExpression { - // numeric_term_with_unary = - // number | numeric_term_without_unary_without_number | numeric_plus_minus_unary_term ; - return when (tokens.peek()) { - is MinusSymbol, is PlusSymbol -> parseNumericPlusMinusUnaryTerm() - is PositiveInteger, is PositiveRealNumber -> parseNumber() - else -> parseNumericTermWithoutUnaryWithoutNumber() + private fun parseGenericTermWithUnary(): MathExpression { + // generic_term_with_unary = + // number | generic_term_without_unary_without_number | generic_plus_minus_unary_term ; + return when { + hasNextGenericPlusMinusUnaryTerm() -> parseGenericPlusMinusUnaryTerm() + hasNextNumber() -> parseNumber() + hasNextGenericTermWithoutUnaryWithoutNumber() -> parseGenericTermWithoutUnaryWithoutNumber() + else -> throw ParseException() + } + } + + private fun hasNextGenericTermWithoutUnaryWithoutNumber(): Boolean { + return when (parseContext) { + ParseContext.NumericExpressionContext -> hasNextNumericTermWithoutUnaryWithoutNumber() + is ParseContext.AlgebraicExpressionContext -> hasNextAlgebraicTermWithoutUnaryWithoutNumber() } } + private fun parseGenericTermWithoutUnaryWithoutNumber(): MathExpression { + // generic_term_without_unary_without_number is either numeric_term_without_unary_without_number + // or algebraic_term_without_unary_without_number based the current parser context. + return when (parseContext) { + ParseContext.NumericExpressionContext -> parseNumericTermWithoutUnaryWithoutNumber() + is ParseContext.AlgebraicExpressionContext -> parseAlgebraicTermWithoutUnaryWithoutNumber() + } + } + + private fun hasNextNumericTermWithoutUnaryWithoutNumber(): Boolean = + hasNextGenericFunctionExpression() + || hasNextGenericGroupExpression() + || hasNextGenericRootedTerm() + private fun parseNumericTermWithoutUnaryWithoutNumber(): MathExpression { // numeric_term_without_unary_without_number = - // numeric_function_expression | numeric_group_expression | numeric_rooted_term ; - return when (tokens.peek()) { - is FunctionName -> parseNumericFunctionExpression() - is LeftParenthesisSymbol -> parseNumericGroupExpression() - is SquareRootSymbol -> parseNumericRootedTerm() + // generic_function_expression | generic_group_expression | generic_rooted_term ; + return when { + hasNextGenericFunctionExpression() -> parseGenericFunctionExpression() + hasNextGenericGroupExpression() -> parseGenericGroupExpression() + hasNextGenericRootedTerm() -> parseGenericRootedTerm() else -> throw ParseException() } } - private fun parseNumber(): MathExpression { - // number = positive_real_number | positive_integer ; - return MathExpression.newBuilder().apply { - constant = when ( - val numberToken = consumeNextTokenMatching { - it is PositiveInteger || it is PositiveRealNumber - } - ) { - is PositiveInteger -> Real.newBuilder().apply { - integer = numberToken.parsedValue - }.build() - is PositiveRealNumber -> Real.newBuilder().apply { - irrational = numberToken.parsedValue - }.build() + private fun hasNextAlgebraicTermWithoutUnaryWithoutNumber(): Boolean = + hasNextGenericFunctionExpression() + || hasNextGenericGroupExpression() + || hasNextGenericRootedTerm() + || hasNextVariable() - // TODO: add error that one of the above was expected. Other error handling should maybe - // happen in the same way. - else -> throw ParseException() // Something went wrong. - } - }.build() + private fun parseAlgebraicTermWithoutUnaryWithoutNumber(): MathExpression { + // algebraic_term_without_unary_without_number = + // generic_function_expression | generic_group_expression | generic_rooted_term | variable ; + return when { + hasNextGenericFunctionExpression() -> parseGenericFunctionExpression() + hasNextGenericGroupExpression() -> parseGenericGroupExpression() + hasNextGenericRootedTerm() -> parseGenericRootedTerm() + hasNextVariable() -> parseVariable() + else -> throw ParseException() + } } - private fun parseNumericFunctionExpression(): MathExpression { - // numeric_function_expression = function_name , left_paren , numeric_expression , right_paren ; + private fun hasNextGenericFunctionExpression(): Boolean = tokens.peek() is FunctionName + + private fun parseGenericFunctionExpression(): MathExpression { + // generic_function_expression = function_name , left_paren , generic_expression , right_paren ; return MathExpression.newBuilder().apply { - val functionName = expectNextTokenWithType() + val functionName = consumeTokenOfType() if (functionName.parsedName != "sqrt") throw ParseException() - consumeTokenOfType { LeftParenthesisSymbol } + consumeTokenOfType() functionCall = MathFunctionCall.newBuilder().apply { functionType = MathFunctionCall.FunctionType.SQUARE_ROOT - argument = parseNumericExpression() + argument = parseGenericExpression() }.build() - consumeTokenOfType { RightParenthesisSymbol } + consumeTokenOfType() }.build() } - private fun parseNumericGroupExpression(): MathExpression { - // numeric_group_expression = left_paren , numeric_expression , right_paren ; - consumeTokenOfType { LeftParenthesisSymbol } - return parseNumericExpression().also { - consumeTokenOfType { RightParenthesisSymbol } + private fun hasNextGenericGroupExpression(): Boolean = tokens.peek() is LeftParenthesisSymbol + + private fun parseGenericGroupExpression(): MathExpression { + // generic_group_expression = left_paren , generic_expression , right_paren ; + consumeTokenOfType() + return parseGenericExpression().also { + consumeTokenOfType() } } - private fun parseNumericPlusMinusUnaryTerm(): MathExpression { - // numeric_plus_minus_unary_term = numeric_negated_term | numeric_positive_term ; - return when (tokens.peek()) { - is MinusSymbol -> parseNumericNegatedTerm() - is PlusSymbol -> parseNumericPositiveTerm() + private fun hasNextGenericPlusMinusUnaryTerm(): Boolean = + hasNextGenericNegatedTerm() || hasNextGenericPositiveTerm() + + private fun parseGenericPlusMinusUnaryTerm(): MathExpression { + // generic_plus_minus_unary_term = generic_negated_term | generic_positive_term ; + return when { + hasNextGenericNegatedTerm() -> parseGenericNegatedTerm() + hasNextGenericPositiveTerm() -> parseGenericPositiveTerm() else -> throw ParseException() } } - private fun parseNumericNegatedTerm(): MathExpression { - // numeric_negated_term = minus_operator , numeric_mult_div_expression ; - consumeTokenOfType { MinusSymbol } + private fun hasNextGenericNegatedTerm(): Boolean = tokens.peek() is MinusSymbol + + private fun parseGenericNegatedTerm(): MathExpression { + // generic_negated_term = minus_operator , generic_mult_div_expression ; + consumeTokenOfType() return MathExpression.newBuilder().apply { unaryOperation = MathUnaryOperation.newBuilder().apply { operator = MathUnaryOperation.Operator.NEGATE - operand = parseNumericMultDivExpression() + operand = parseGenericMultDivExpression() }.build() }.build() } - private fun parseNumericPositiveTerm(): MathExpression { - // numeric_positive_term = plus_operator , numeric_mult_div_expression ; - consumeTokenOfType { PlusSymbol } + private fun hasNextGenericPositiveTerm(): Boolean = tokens.peek() is PlusSymbol + + private fun parseGenericPositiveTerm(): MathExpression { + // generic_positive_term = plus_operator , generic_mult_div_expression ; + consumeTokenOfType() return MathExpression.newBuilder().apply { unaryOperation = MathUnaryOperation.newBuilder().apply { operator = MathUnaryOperation.Operator.POSITIVE - operand = parseNumericMultDivExpression() + operand = parseGenericMultDivExpression() }.build() }.build() } - private fun parseNumericRootedTerm(): MathExpression { - // numeric_rooted_term = square_root_operator , numeric_term_with_unary ; - consumeTokenOfType { SquareRootSymbol } + private fun hasNextGenericRootedTerm(): Boolean = tokens.peek() is SquareRootSymbol + + private fun parseGenericRootedTerm(): MathExpression { + // generic_rooted_term = square_root_operator , generic_term_with_unary ; + consumeTokenOfType() return MathExpression.newBuilder().apply { functionCall = MathFunctionCall.newBuilder().apply { functionType = MathFunctionCall.FunctionType.SQUARE_ROOT - argument = parseNumericTermWithUnary() + argument = parseGenericTermWithUnary() }.build() }.build() } - private inline fun expectNextTokenWithType(): T { - return (tokens.next() as? T) ?: throw ParseException() + private fun hasNextNumber(): Boolean = hasNextPositiveInteger() || hasNextPositiveRealNumber() + + private fun parseNumber(): MathExpression { + // number = positive_real_number | positive_integer ; + return MathExpression.newBuilder().apply { + constant = when { + hasNextPositiveInteger() -> Real.newBuilder().apply { + integer = consumeTokenOfType().parsedValue + }.build() + hasNextPositiveRealNumber() -> Real.newBuilder().apply { + irrational = consumeTokenOfType().parsedValue + }.build() + // TODO: add error that one of the above was expected. Other error handling should maybe + // happen in the same way. + else -> throw ParseException() // Something went wrong. + } + }.build() } - private inline fun consumeTokenOfType(noinline expected: () -> T): T { - return (tokens.expectNextValue(expected) as? T) ?: throw ParseException() + private fun hasNextPositiveInteger(): Boolean = tokens.peek() is PositiveInteger + + private fun hasNextPositiveRealNumber(): Boolean = tokens.peek() is PositiveRealNumber + + private fun hasNextVariable(): Boolean = tokens.peek() is VariableName + + private fun parseVariable(): MathExpression { + val variableName = consumeTokenOfType() + if (!parseContext.allowsVariable(variableName.parsedName)) { + throw ParseException() + } + return MathExpression.newBuilder().apply { + variable = variableName.parsedName + }.build() } - private fun consumeNextTokenMatching(predicate: (Token) -> Boolean): Token { - return tokens.expectNextMatches(predicate) ?: throw ParseException() + private inline fun consumeTokenOfType(): T { + return (tokens.expectNextMatches { it is T } as? T) ?: throw ParseException() } // TODO: do error handling better than this (& in a way that works better with the types of errors // that we want to show users). class ParseException : Exception() + + private sealed class ParseContext { + abstract fun allowsVariable(variableName: String): Boolean + + object NumericExpressionContext : ParseContext() { + // Numeric expressions never allow variables. + override fun allowsVariable(variableName: String): Boolean = false + } + + data class AlgebraicExpressionContext( + private val allowedVariables: List + ) : ParseContext() { + override fun allowsVariable(variableName: String): Boolean = variableName in allowedVariables + } + } + + companion object { + fun parseNumericExpression(rawExpression: String): MathExpression = + NumericExpressionParser(rawExpression, ParseContext.NumericExpressionContext).parseGeneric() + + fun parseAlgebraicExpression( + rawExpression: String, allowedVariables: List + ): MathExpression { + val parser = + NumericExpressionParser( + rawExpression, ParseContext.AlgebraicExpressionContext(allowedVariables) + ) + return parser.parseGeneric() + } + } } diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 1342042dae7..5c36b3906d6 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -5,6 +5,7 @@ import com.google.common.truth.BooleanSubject import com.google.common.truth.DoubleSubject import com.google.common.truth.FailureMetadata import com.google.common.truth.IntegerSubject +import com.google.common.truth.StringSubject import com.google.common.truth.Subject import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat @@ -25,18 +26,19 @@ import org.oppia.android.app.model.Real import org.oppia.android.testing.assertThrows import org.robolectric.annotation.LooperMode import kotlin.math.sqrt +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE /** Tests for [MathExpressionParser]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class NumericExpressionParserTest { @Test - fun testLotsOfCases() { + fun testLotsOfCasesForNumericExpression() { // TODO: split this up // TODO: add log string generation for expressions. - expectFailureWhenParsing("") + expectFailureWhenParsingNumericExpression("") - val expression1 = parseExpression("1") + val expression1 = parseNumericExpression("1") assertThat(expression1).hasStructureThatMatches { constant { withIntegerValueThat().isEqualTo(1) @@ -44,9 +46,9 @@ class NumericExpressionParserTest { } assertThat(expression1).evaluatesToIntegerThat().isEqualTo(1) - expectFailureWhenParsing("x") + expectFailureWhenParsingNumericExpression("x") - val expression2 = parseExpression(" 2 ") + val expression2 = parseNumericExpression(" 2 ") assertThat(expression2).hasStructureThatMatches { constant { withIntegerValueThat().isEqualTo(2) @@ -54,7 +56,7 @@ class NumericExpressionParserTest { } assertThat(expression2).evaluatesToIntegerThat().isEqualTo(2) - val expression3 = parseExpression(" 2.5 ") + val expression3 = parseNumericExpression(" 2.5 ") assertThat(expression3).hasStructureThatMatches { constant { withIrrationalValueThat().isWithin(1e-5).of(2.5) @@ -62,11 +64,11 @@ class NumericExpressionParserTest { } assertThat(expression3).evaluatesToIrrationalThat().isWithin(1e-5).of(2.5) - expectFailureWhenParsing(" x ") + expectFailureWhenParsingNumericExpression(" x ") - expectFailureWhenParsing(" z x ") + expectFailureWhenParsingNumericExpression(" z x ") - val expression4 = parseExpression("2^3^2") + val expression4 = parseNumericExpression("2^3^2") assertThat(expression4).hasStructureThatMatches { exponentiation { leftOperand { @@ -92,7 +94,7 @@ class NumericExpressionParserTest { } assertThat(expression4).evaluatesToIntegerThat().isEqualTo(512) - val expression23 = parseExpression("(2^3)^2") + val expression23 = parseNumericExpression("(2^3)^2") assertThat(expression23).hasStructureThatMatches { exponentiation { leftOperand { @@ -118,7 +120,7 @@ class NumericExpressionParserTest { } assertThat(expression23).evaluatesToIntegerThat().isEqualTo(64) - val expression24 = parseExpression("512/32/4") + val expression24 = parseNumericExpression("512/32/4") assertThat(expression24).hasStructureThatMatches { division { leftOperand { @@ -144,7 +146,7 @@ class NumericExpressionParserTest { } assertThat(expression24).evaluatesToIntegerThat().isEqualTo(4) - val expression25 = parseExpression("512/(32/4)") + val expression25 = parseNumericExpression("512/(32/4)") assertThat(expression25).hasStructureThatMatches { division { leftOperand { @@ -170,7 +172,7 @@ class NumericExpressionParserTest { } assertThat(expression25).evaluatesToIntegerThat().isEqualTo(64) - val expression5 = parseExpression("sqrt(2)") + val expression5 = parseNumericExpression("sqrt(2)") assertThat(expression5).hasStructureThatMatches { functionCallTo(SQUARE_ROOT) { argument { @@ -182,11 +184,11 @@ class NumericExpressionParserTest { } assertThat(expression5).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0)) - expectFailureWhenParsing("sqr(2)") + expectFailureWhenParsingNumericExpression("sqr(2)") - expectFailureWhenParsing("xyz(2)") + expectFailureWhenParsingNumericExpression("xyz(2)") - val expression6 = parseExpression("732") + val expression6 = parseNumericExpression("732") assertThat(expression6).hasStructureThatMatches { constant { withIntegerValueThat().isEqualTo(732) @@ -194,10 +196,10 @@ class NumericExpressionParserTest { } assertThat(expression6).evaluatesToIntegerThat().isEqualTo(732) - expectFailureWhenParsing("73 2") + expectFailureWhenParsingNumericExpression("73 2") // Verify order of operations between higher & lower precedent operators. - val expression32 = parseExpression("3+4^7") + val expression32 = parseNumericExpression("3+4^7") assertThat(expression32).hasStructureThatMatches { addition { leftOperand { @@ -223,7 +225,7 @@ class NumericExpressionParserTest { } assertThat(expression32).evaluatesToIntegerThat().isEqualTo(16387) - val expression7 = parseExpression("3*2-3+4^7*8/3*2+7") + val expression7 = parseNumericExpression("3*2-3+4^7*8/3*2+7") assertThat(expression7).hasStructureThatMatches { // To better visualize the precedence & order of operations, see this grouped version: // (((3*2)-3)+((((4^7)*8)/3)*2))+7. @@ -325,9 +327,9 @@ class NumericExpressionParserTest { .isWithin(1e-5) .of(87391.333333333) - expectFailureWhenParsing("x = √2 × 7 ÷ 4") + expectFailureWhenParsingNumericExpression("x = √2 × 7 ÷ 4") - val expression8 = parseExpression("(1+2)(3+4)") + val expression8 = parseNumericExpression("(1+2)(3+4)") assertThat(expression8).hasStructureThatMatches { multiplication { leftOperand { @@ -363,9 +365,9 @@ class NumericExpressionParserTest { assertThat(expression8).evaluatesToIntegerThat().isEqualTo(21) // Right implicit multiplication of numbers isn't allowed. - expectFailureWhenParsing("(1+2)2") + expectFailureWhenParsingNumericExpression("(1+2)2") - val expression10 = parseExpression("2(1+2)") + val expression10 = parseNumericExpression("2(1+2)") assertThat(expression10).hasStructureThatMatches { multiplication { leftOperand { @@ -392,9 +394,9 @@ class NumericExpressionParserTest { assertThat(expression10).evaluatesToIntegerThat().isEqualTo(6) // Right implicit multiplication of numbers isn't allowed. - expectFailureWhenParsing("sqrt(2)3") + expectFailureWhenParsingNumericExpression("sqrt(2)3") - val expression12 = parseExpression("3sqrt(2)") + val expression12 = parseNumericExpression("3sqrt(2)") assertThat(expression12).hasStructureThatMatches { multiplication { leftOperand { @@ -415,9 +417,9 @@ class NumericExpressionParserTest { } assertThat(expression12).evaluatesToIrrationalThat().isWithin(1e-5).of(3.0 * sqrt(2.0)) - expectFailureWhenParsing("xsqrt(2)") + expectFailureWhenParsingNumericExpression("xsqrt(2)") - val expression13 = parseExpression("sqrt(2)*(1+2)*(3-2^5)") + val expression13 = parseNumericExpression("sqrt(2)*(1+2)*(3-2^5)") assertThat(expression13).hasStructureThatMatches { multiplication { leftOperand { @@ -474,7 +476,7 @@ class NumericExpressionParserTest { } assertThat(expression13).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) - val expression58 = parseExpression("sqrt(2)(1+2)(3-2^5)") + val expression58 = parseNumericExpression("sqrt(2)(1+2)(3-2^5)") assertThat(expression58).hasStructureThatMatches { multiplication { leftOperand { @@ -531,7 +533,7 @@ class NumericExpressionParserTest { } assertThat(expression58).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) - val expression14 = parseExpression("((3))") + val expression14 = parseNumericExpression("((3))") assertThat(expression14).hasStructureThatMatches { constant { withIntegerValueThat().isEqualTo(3) @@ -539,7 +541,7 @@ class NumericExpressionParserTest { } assertThat(expression14).evaluatesToIntegerThat().isEqualTo(3) - val expression15 = parseExpression("++3") + val expression15 = parseNumericExpression("++3") assertThat(expression15).hasStructureThatMatches { positive { operand { @@ -555,7 +557,7 @@ class NumericExpressionParserTest { } assertThat(expression15).evaluatesToIntegerThat().isEqualTo(3) - val expression16 = parseExpression("--4") + val expression16 = parseNumericExpression("--4") assertThat(expression16).hasStructureThatMatches { negation { operand { @@ -571,7 +573,7 @@ class NumericExpressionParserTest { } assertThat(expression16).evaluatesToIntegerThat().isEqualTo(4) - val expression17 = parseExpression("1+-4") + val expression17 = parseNumericExpression("1+-4") assertThat(expression17).hasStructureThatMatches { addition { leftOperand { @@ -592,7 +594,7 @@ class NumericExpressionParserTest { } assertThat(expression17).evaluatesToIntegerThat().isEqualTo(-3) - val expression18 = parseExpression("1++4") + val expression18 = parseNumericExpression("1++4") assertThat(expression18).hasStructureThatMatches { addition { leftOperand { @@ -613,7 +615,7 @@ class NumericExpressionParserTest { } assertThat(expression18).evaluatesToIntegerThat().isEqualTo(5) - val expression19 = parseExpression("1--4") + val expression19 = parseNumericExpression("1--4") assertThat(expression19).hasStructureThatMatches { subtraction { leftOperand { @@ -634,9 +636,9 @@ class NumericExpressionParserTest { } assertThat(expression19).evaluatesToIntegerThat().isEqualTo(5) - expectFailureWhenParsing("1-^-4") + expectFailureWhenParsingNumericExpression("1-^-4") - val expression20 = parseExpression("√2 × 7 ÷ 4") + val expression20 = parseNumericExpression("√2 × 7 ÷ 4") assertThat(expression20).hasStructureThatMatches { division { leftOperand { @@ -666,9 +668,9 @@ class NumericExpressionParserTest { } assertThat(expression20).evaluatesToIrrationalThat().isWithin(1e-5).of((sqrt(2.0) * 7.0) / 4.0) - expectFailureWhenParsing("1+2 asdf") + expectFailureWhenParsingNumericExpression("1+2 &asdf") - val expression21 = parseExpression("sqrt(2)sqrt(3)sqrt(4)") + val expression21 = parseNumericExpression("sqrt(2)sqrt(3)sqrt(4)") // Note that this tree demonstrates left associativity. assertThat(expression21).hasStructureThatMatches { multiplication { @@ -710,7 +712,7 @@ class NumericExpressionParserTest { .isWithin(1e-5) .of(sqrt(2.0) * sqrt(3.0) * sqrt(4.0)) - val expression22 = parseExpression("(1+2)(3-7^2)(5+-17)") + val expression22 = parseNumericExpression("(1+2)(3-7^2)(5+-17)") assertThat(expression22).hasStructureThatMatches { multiplication { leftOperand { @@ -779,7 +781,7 @@ class NumericExpressionParserTest { } assertThat(expression22).evaluatesToIntegerThat().isEqualTo(1656) - val expression26 = parseExpression("3^-2") + val expression26 = parseNumericExpression("3^-2") assertThat(expression26).hasStructureThatMatches { exponentiation { leftOperand { @@ -805,7 +807,7 @@ class NumericExpressionParserTest { hasDenominatorThat().isEqualTo(9) } - val expression27 = parseExpression("(3^-2)^(3^-2)") + val expression27 = parseNumericExpression("(3^-2)^(3^-2)") assertThat(expression27).hasStructureThatMatches { exponentiation { leftOperand { @@ -848,7 +850,7 @@ class NumericExpressionParserTest { } assertThat(expression27).evaluatesToIrrationalThat().isWithin(1e-5).of(0.78338103693) - val expression28 = parseExpression("1-3^sqrt(4)") + val expression28 = parseNumericExpression("1-3^sqrt(4)") assertThat(expression28).hasStructureThatMatches { subtraction { leftOperand { @@ -880,7 +882,7 @@ class NumericExpressionParserTest { // "Hard" order of operation problems loosely based on & other problems that can often stump // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. - val expression29 = parseExpression("3÷2*(3+4)") + val expression29 = parseNumericExpression("3÷2*(3+4)") assertThat(expression29).hasStructureThatMatches { multiplication { leftOperand { @@ -915,7 +917,7 @@ class NumericExpressionParserTest { } assertThat(expression29).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) - val expression59 = parseExpression("3÷2(3+4)") + val expression59 = parseNumericExpression("3÷2(3+4)") assertThat(expression59).hasStructureThatMatches { multiplication { leftOperand { @@ -951,13 +953,13 @@ class NumericExpressionParserTest { assertThat(expression59).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) // Numbers cannot have implicit multiplication unless they are in groups. - expectFailureWhenParsing("2 2") + expectFailureWhenParsingNumericExpression("2 2") - expectFailureWhenParsing("2 2^2") + expectFailureWhenParsingNumericExpression("2 2^2") - expectFailureWhenParsing("2^2 2") + expectFailureWhenParsingNumericExpression("2^2 2") - val expression31 = parseExpression("(3)(4)(5)") + val expression31 = parseNumericExpression("(3)(4)(5)") assertThat(expression31).hasStructureThatMatches { multiplication { leftOperand { @@ -983,7 +985,7 @@ class NumericExpressionParserTest { } assertThat(expression31).evaluatesToIntegerThat().isEqualTo(60) - val expression33 = parseExpression("2^(3)") + val expression33 = parseNumericExpression("2^(3)") assertThat(expression33).hasStructureThatMatches { exponentiation { leftOperand { @@ -1001,7 +1003,7 @@ class NumericExpressionParserTest { assertThat(expression33).evaluatesToIntegerThat().isEqualTo(8) // Verify that implicit multiple has lower precedence than exponentiation. - val expression34 = parseExpression("2^(3)(4)") + val expression34 = parseNumericExpression("2^(3)(4)") assertThat(expression34).hasStructureThatMatches { multiplication { leftOperand { @@ -1028,9 +1030,9 @@ class NumericExpressionParserTest { assertThat(expression34).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can never be an implicit right operand. - expectFailureWhenParsing("2^(3)2^2") + expectFailureWhenParsingNumericExpression("2^(3)2^2") - val expression35 = parseExpression("2^(3)*2^2") + val expression35 = parseNumericExpression("2^(3)*2^2") assertThat(expression35).hasStructureThatMatches { multiplication { leftOperand { @@ -1066,7 +1068,7 @@ class NumericExpressionParserTest { assertThat(expression35).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can be a right operand of an implicit mult if it's grouped. - val expression36 = parseExpression("2^(3)(2^2)") + val expression36 = parseNumericExpression("2^(3)(2^2)") assertThat(expression36).hasStructureThatMatches { multiplication { leftOperand { @@ -1102,9 +1104,9 @@ class NumericExpressionParserTest { assertThat(expression36).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can never be an implicit right operand. - expectFailureWhenParsing("2^3(4)2^3") + expectFailureWhenParsingNumericExpression("2^3(4)2^3") - val expression38 = parseExpression("2^3(4)*2^3") + val expression38 = parseNumericExpression("2^3(4)*2^3") assertThat(expression38).hasStructureThatMatches { // 2^3(4)*2^3 multiplication { @@ -1157,15 +1159,15 @@ class NumericExpressionParserTest { } assertThat(expression38).evaluatesToIntegerThat().isEqualTo(256) - expectFailureWhenParsing("2^2 2^2") - expectFailureWhenParsing("(3) 2^2") - expectFailureWhenParsing("sqrt(3) 2^2") - expectFailureWhenParsing("√2 2^2") - expectFailureWhenParsing("2^2 3") + expectFailureWhenParsingNumericExpression("2^2 2^2") + expectFailureWhenParsingNumericExpression("(3) 2^2") + expectFailureWhenParsingNumericExpression("sqrt(3) 2^2") + expectFailureWhenParsingNumericExpression("√2 2^2") + expectFailureWhenParsingNumericExpression("2^2 3") - expectFailureWhenParsing("-2 3") + expectFailureWhenParsingNumericExpression("-2 3") - val expression39 = parseExpression("-(1+2)") + val expression39 = parseNumericExpression("-(1+2)") assertThat(expression39).hasStructureThatMatches { negation { operand { @@ -1187,9 +1189,9 @@ class NumericExpressionParserTest { assertThat(expression39).evaluatesToIntegerThat().isEqualTo(-3) // Should pass for algebra. - expectFailureWhenParsing("-2 x") + expectFailureWhenParsingNumericExpression("-2 x") - val expression40 = parseExpression("-2 (1+2)") + val expression40 = parseNumericExpression("-2 (1+2)") assertThat(expression40).hasStructureThatMatches { // The negation happens last for parity with other common calculators. negation { @@ -1220,7 +1222,7 @@ class NumericExpressionParserTest { } assertThat(expression40).evaluatesToIntegerThat().isEqualTo(-6) - val expression41 = parseExpression("-2^3(4)") + val expression41 = parseNumericExpression("-2^3(4)") assertThat(expression41).hasStructureThatMatches { negation { operand { @@ -1250,7 +1252,7 @@ class NumericExpressionParserTest { } assertThat(expression41).evaluatesToIntegerThat().isEqualTo(-32) - val expression43 = parseExpression("√2^2(3)") + val expression43 = parseNumericExpression("√2^2(3)") assertThat(expression43).hasStructureThatMatches { multiplication { leftOperand { @@ -1280,7 +1282,7 @@ class NumericExpressionParserTest { } assertThat(expression43).evaluatesToIrrationalThat().isWithin(1e-5).of(6.0) - val expression60 = parseExpression("√(2^2(3))") + val expression60 = parseNumericExpression("√(2^2(3))") assertThat(expression60).hasStructureThatMatches { functionCallTo(SQUARE_ROOT) { argument { @@ -1310,7 +1312,7 @@ class NumericExpressionParserTest { } assertThat(expression60).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(12.0)) - val expression42 = parseExpression("-2*-2") + val expression42 = parseNumericExpression("-2*-2") // Note that the following structure is not the same as (-2)*(-2) since unary negation has // higher precedence than multiplication, so it's first & recurses to include the entire // multiplication expression. @@ -1338,7 +1340,7 @@ class NumericExpressionParserTest { } assertThat(expression42).evaluatesToIntegerThat().isEqualTo(4) - val expression44 = parseExpression("2(2)") + val expression44 = parseNumericExpression("2(2)") assertThat(expression44).hasStructureThatMatches { multiplication { leftOperand { @@ -1355,7 +1357,7 @@ class NumericExpressionParserTest { } assertThat(expression44).evaluatesToIntegerThat().isEqualTo(4) - val expression45 = parseExpression("2sqrt(2)") + val expression45 = parseNumericExpression("2sqrt(2)") assertThat(expression45).hasStructureThatMatches { multiplication { leftOperand { @@ -1376,7 +1378,7 @@ class NumericExpressionParserTest { } assertThat(expression45).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) - val expression46 = parseExpression("2√2") + val expression46 = parseNumericExpression("2√2") assertThat(expression46).hasStructureThatMatches { multiplication { leftOperand { @@ -1397,7 +1399,7 @@ class NumericExpressionParserTest { } assertThat(expression46).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) - val expression47 = parseExpression("(2)(2)") + val expression47 = parseNumericExpression("(2)(2)") assertThat(expression47).hasStructureThatMatches { multiplication { leftOperand { @@ -1414,7 +1416,7 @@ class NumericExpressionParserTest { } assertThat(expression47).evaluatesToIntegerThat().isEqualTo(4) - val expression48 = parseExpression("sqrt(2)(2)") + val expression48 = parseNumericExpression("sqrt(2)(2)") assertThat(expression48).hasStructureThatMatches { multiplication { leftOperand { @@ -1435,7 +1437,7 @@ class NumericExpressionParserTest { } assertThat(expression48).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) - val expression49 = parseExpression("sqrt(2)sqrt(2)") + val expression49 = parseNumericExpression("sqrt(2)sqrt(2)") assertThat(expression49).hasStructureThatMatches { multiplication { leftOperand { @@ -1460,7 +1462,7 @@ class NumericExpressionParserTest { } assertThat(expression49).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) - val expression50 = parseExpression("√2√2") + val expression50 = parseNumericExpression("√2√2") assertThat(expression50).hasStructureThatMatches { multiplication { leftOperand { @@ -1485,7 +1487,7 @@ class NumericExpressionParserTest { } assertThat(expression50).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) - val expression51 = parseExpression("(2)(2)(2)") + val expression51 = parseNumericExpression("(2)(2)(2)") assertThat(expression51).hasStructureThatMatches { multiplication { leftOperand { @@ -1511,7 +1513,7 @@ class NumericExpressionParserTest { } assertThat(expression51).evaluatesToIntegerThat().isEqualTo(8) - val expression52 = parseExpression("sqrt(2)sqrt(2)sqrt(2)") + val expression52 = parseNumericExpression("sqrt(2)sqrt(2)sqrt(2)") assertThat(expression52).hasStructureThatMatches { multiplication { leftOperand { @@ -1550,7 +1552,7 @@ class NumericExpressionParserTest { val sqrt2 = sqrt(2.0) assertThat(expression52).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) - val expression53 = parseExpression("√2√2√2") + val expression53 = parseNumericExpression("√2√2√2") assertThat(expression53).hasStructureThatMatches { multiplication { leftOperand { @@ -1589,12 +1591,12 @@ class NumericExpressionParserTest { assertThat(expression53).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) // Should fail for algebra. - expectFailureWhenParsing("x7") + expectFailureWhenParsingNumericExpression("x7") // Should pass for algebra. - expectFailureWhenParsing("2x^2") + expectFailureWhenParsingNumericExpression("2x^2") - val expression54 = parseExpression("2*2/-4+7*2") + val expression54 = parseNumericExpression("2*2/-4+7*2") assertThat(expression54).hasStructureThatMatches { // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) addition { @@ -1652,7 +1654,7 @@ class NumericExpressionParserTest { } assertThat(expression54).evaluatesToIntegerThat().isEqualTo(13) - val expression55 = parseExpression("(3/(1-2))") + val expression55 = parseNumericExpression("(3/(1-2))") assertThat(expression55).hasStructureThatMatches { division { leftOperand { @@ -1678,7 +1680,7 @@ class NumericExpressionParserTest { } assertThat(expression55).evaluatesToIntegerThat().isEqualTo(-3) - val expression56 = parseExpression("(3)/(1-2)") + val expression56 = parseNumericExpression("(3)/(1-2)") assertThat(expression56).hasStructureThatMatches { division { leftOperand { @@ -1704,7 +1706,1858 @@ class NumericExpressionParserTest { } assertThat(expression56).evaluatesToIntegerThat().isEqualTo(-3) - val expression57 = parseExpression("3/((1-2))") + val expression57 = parseNumericExpression("3/((1-2))") + assertThat(expression57).hasStructureThatMatches { + division { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression57).evaluatesToIntegerThat().isEqualTo(-3) + + // TODO: add others, including tests for malformed expressions throughout the parser & + // tokenizer. + } + + @Test + fun testLotsOfCasesForAlgebraicExpression() { + // TODO: split this up + // TODO: add log string generation for expressions. + expectFailureWhenParsingAlgebraicExpression("") + + val expression1 = parseAlgebraicExpression("1") + assertThat(expression1).hasStructureThatMatches { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + assertThat(expression1).evaluatesToIntegerThat().isEqualTo(1) + + val expression61 = parseAlgebraicExpression("x") + assertThat(expression61).hasStructureThatMatches { + variable { + withNameThat().isEqualTo("x") + } + } + + val expression2 = parseAlgebraicExpression(" 2 ") + assertThat(expression2).hasStructureThatMatches { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + assertThat(expression2).evaluatesToIntegerThat().isEqualTo(2) + + val expression3 = parseAlgebraicExpression(" 2.5 ") + assertThat(expression3).hasStructureThatMatches { + constant { + withIrrationalValueThat().isWithin(1e-5).of(2.5) + } + } + assertThat(expression3).evaluatesToIrrationalThat().isWithin(1e-5).of(2.5) + + val expression62 = parseAlgebraicExpression(" y ") + assertThat(expression62).hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + + val expression63 = parseAlgebraicExpression(" z x ") + assertThat(expression63).hasStructureThatMatches { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("z") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + + val expression4 = parseAlgebraicExpression("2^3^2") + assertThat(expression4).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression4).evaluatesToIntegerThat().isEqualTo(512) + + val expression23 = parseAlgebraicExpression("(2^3)^2") + assertThat(expression23).hasStructureThatMatches { + exponentiation { + leftOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + assertThat(expression23).evaluatesToIntegerThat().isEqualTo(64) + + val expression24 = parseAlgebraicExpression("512/32/4") + assertThat(expression24).hasStructureThatMatches { + division { + leftOperand { + division { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(512) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(32) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + assertThat(expression24).evaluatesToIntegerThat().isEqualTo(4) + + val expression25 = parseAlgebraicExpression("512/(32/4)") + assertThat(expression25).hasStructureThatMatches { + division { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(512) + } + } + rightOperand { + division { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(32) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression25).evaluatesToIntegerThat().isEqualTo(64) + + val expression5 = parseAlgebraicExpression("sqrt(2)") + assertThat(expression5).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + assertThat(expression5).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0)) + + expectFailureWhenParsingAlgebraicExpression("sqr(2)") + + val expression64 = parseAlgebraicExpression("xyz(2)") + assertThat(expression64).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("y") + } + } + } + } + rightOperand { + variable { + withNameThat().isEqualTo("z") + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + + val expression6 = parseAlgebraicExpression("732") + assertThat(expression6).hasStructureThatMatches { + constant { + withIntegerValueThat().isEqualTo(732) + } + } + assertThat(expression6).evaluatesToIntegerThat().isEqualTo(732) + + expectFailureWhenParsingAlgebraicExpression("73 2") + + // Verify order of operations between higher & lower precedent operators. + val expression32 = parseAlgebraicExpression("3+4^7") + assertThat(expression32).hasStructureThatMatches { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(7) + } + } + } + } + } + } + assertThat(expression32).evaluatesToIntegerThat().isEqualTo(16387) + + val expression7 = parseAlgebraicExpression("3*2-3+4^7*8/3*2+7") + assertThat(expression7).hasStructureThatMatches { + // To better visualize the precedence & order of operations, see this grouped version: + // (((3*2)-3)+((((4^7)*8)/3)*2))+7. + addition { + leftOperand { + // ((3*2)-3)+((((4^7)*8)/3)*2) + addition { + leftOperand { + // (1*2)-3 + subtraction { + leftOperand { + // 3*2 + multiplication { + leftOperand { + // 3 + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + // 2 + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + // 3 + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + rightOperand { + // (((4^7)*8)/3)*2 + multiplication { + leftOperand { + // ((4^7)*8)/3 + division { + leftOperand { + // (4^7)*8 + multiplication { + leftOperand { + // 4^7 + exponentiation { + leftOperand { + // 4 + constant { + withIntegerValueThat().isEqualTo(4) + } + } + rightOperand { + // 7 + constant { + withIntegerValueThat().isEqualTo(7) + } + } + } + } + rightOperand { + // 8 + constant { + withIntegerValueThat().isEqualTo(8) + } + } + } + } + rightOperand { + // 3 + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 2 + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + // 7 + constant { + withIntegerValueThat().isEqualTo(7) + } + } + } + } + assertThat(expression7) + .evaluatesToRationalThat() + .evaluatesToRealThat() + .isWithin(1e-5) + .of(87391.333333333) + + expectFailureWhenParsingAlgebraicExpression("x = √2 × 7 ÷ 4") + + val expression8 = parseAlgebraicExpression("(1+2)(3+4)") + assertThat(expression8).hasStructureThatMatches { + multiplication { + leftOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression8).evaluatesToIntegerThat().isEqualTo(21) + + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsingAlgebraicExpression("(1+2)2") + + val expression10 = parseAlgebraicExpression("2(1+2)") + assertThat(expression10).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression10).evaluatesToIntegerThat().isEqualTo(6) + + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsingAlgebraicExpression("sqrt(2)3") + + val expression12 = parseAlgebraicExpression("3sqrt(2)") + assertThat(expression12).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression12).evaluatesToIrrationalThat().isWithin(1e-5).of(3.0 * sqrt(2.0)) + + val expression65 = parseAlgebraicExpression("xsqrt(2)") + assertThat(expression65).hasStructureThatMatches { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + + val expression13 = parseAlgebraicExpression("sqrt(2)*(1+2)*(3-2^5)") + assertThat(expression13).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(5) + } + } + } + } + } + } + } + } + assertThat(expression13).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) + + val expression58 = parseAlgebraicExpression("sqrt(2)(1+2)(3-2^5)") + assertThat(expression58).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(5) + } + } + } + } + } + } + } + } + assertThat(expression58).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) + + val expression14 = parseAlgebraicExpression("((3))") + assertThat(expression14).hasStructureThatMatches { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + assertThat(expression14).evaluatesToIntegerThat().isEqualTo(3) + + val expression15 = parseAlgebraicExpression("++3") + assertThat(expression15).hasStructureThatMatches { + positive { + operand { + positive { + operand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + } + } + assertThat(expression15).evaluatesToIntegerThat().isEqualTo(3) + + val expression16 = parseAlgebraicExpression("--4") + assertThat(expression16).hasStructureThatMatches { + negation { + operand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression16).evaluatesToIntegerThat().isEqualTo(4) + + val expression17 = parseAlgebraicExpression("1+-4") + assertThat(expression17).hasStructureThatMatches { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression17).evaluatesToIntegerThat().isEqualTo(-3) + + val expression18 = parseAlgebraicExpression("1++4") + assertThat(expression18).hasStructureThatMatches { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + positive { + operand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression18).evaluatesToIntegerThat().isEqualTo(5) + + val expression19 = parseAlgebraicExpression("1--4") + assertThat(expression19).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression19).evaluatesToIntegerThat().isEqualTo(5) + + expectFailureWhenParsingAlgebraicExpression("1-^-4") + + val expression20 = parseAlgebraicExpression("√2 × 7 ÷ 4") + assertThat(expression20).hasStructureThatMatches { + division { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(7) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + assertThat(expression20).evaluatesToIrrationalThat().isWithin(1e-5).of((sqrt(2.0) * 7.0) / 4.0) + + expectFailureWhenParsingAlgebraicExpression("1+2 &asdf") + + val expression21 = parseAlgebraicExpression("sqrt(2)sqrt(3)sqrt(4)") + // Note that this tree demonstrates left associativity. + assertThat(expression21).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression21) + .evaluatesToIrrationalThat() + .isWithin(1e-5) + .of(sqrt(2.0) * sqrt(3.0) * sqrt(4.0)) + + val expression22 = parseAlgebraicExpression("(1+2)(3-7^2)(5+-17)") + assertThat(expression22).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + // 1+2 + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + // 3-7^2 + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(7) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + } + } + rightOperand { + // 5+-17 + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(5) + } + } + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(17) + } + } + } + } + } + } + } + } + assertThat(expression22).evaluatesToIntegerThat().isEqualTo(1656) + + val expression26 = parseAlgebraicExpression("3^-2") + assertThat(expression26).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression26).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(9) + } + + val expression27 = parseAlgebraicExpression("(3^-2)^(3^-2)") + assertThat(expression27).hasStructureThatMatches { + exponentiation { + leftOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + } + } + assertThat(expression27).evaluatesToIrrationalThat().isWithin(1e-5).of(0.78338103693) + + val expression28 = parseAlgebraicExpression("1-3^sqrt(4)") + assertThat(expression28).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + } + } + assertThat(expression28).evaluatesToIntegerThat().isEqualTo(-8) + + // "Hard" order of operation problems loosely based on & other problems that can often stump + // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. + val expression29 = parseAlgebraicExpression("3÷2*(3+4)") + assertThat(expression29).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression29).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) + + val expression59 = parseAlgebraicExpression("3÷2(3+4)") + assertThat(expression59).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression59).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) + + // Numbers cannot have implicit multiplication unless they are in groups. + expectFailureWhenParsingAlgebraicExpression("2 2") + + expectFailureWhenParsingAlgebraicExpression("2 2^2") + + expectFailureWhenParsingAlgebraicExpression("2^2 2") + + val expression31 = parseAlgebraicExpression("(3)(4)(5)") + assertThat(expression31).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(5) + } + } + } + } + assertThat(expression31).evaluatesToIntegerThat().isEqualTo(60) + + val expression33 = parseAlgebraicExpression("2^(3)") + assertThat(expression33).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + assertThat(expression33).evaluatesToIntegerThat().isEqualTo(8) + + // Verify that implicit multiple has lower precedence than exponentiation. + val expression34 = parseAlgebraicExpression("2^(3)(4)") + assertThat(expression34).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + assertThat(expression34).evaluatesToIntegerThat().isEqualTo(32) + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsingAlgebraicExpression("2^(3)2^2") + + val expression35 = parseAlgebraicExpression("2^(3)*2^2") + assertThat(expression35).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression35).evaluatesToIntegerThat().isEqualTo(32) + + // An exponentiation can be a right operand of an implicit mult if it's grouped. + val expression36 = parseAlgebraicExpression("2^(3)(2^2)") + assertThat(expression36).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression36).evaluatesToIntegerThat().isEqualTo(32) + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsingAlgebraicExpression("2^3(4)2^3") + + val expression38 = parseAlgebraicExpression("2^3(4)*2^3") + assertThat(expression38).hasStructureThatMatches { + // 2^3(4)*2^3 + multiplication { + leftOperand { + // 2^3(4) + multiplication { + leftOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 4 + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + rightOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + } + } + assertThat(expression38).evaluatesToIntegerThat().isEqualTo(256) + + expectFailureWhenParsingAlgebraicExpression("2^2 2^2") + expectFailureWhenParsingAlgebraicExpression("(3) 2^2") + expectFailureWhenParsingAlgebraicExpression("sqrt(3) 2^2") + expectFailureWhenParsingAlgebraicExpression("√2 2^2") + expectFailureWhenParsingAlgebraicExpression("2^2 3") + + expectFailureWhenParsingAlgebraicExpression("-2 3") + + val expression39 = parseAlgebraicExpression("-(1+2)") + assertThat(expression39).hasStructureThatMatches { + negation { + operand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression39).evaluatesToIntegerThat().isEqualTo(-3) + + // Should pass for algebra. + val expression66 = parseAlgebraicExpression("-2 x") + assertThat(expression66).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + } + } + + val expression40 = parseAlgebraicExpression("-2 (1+2)") + assertThat(expression40).hasStructureThatMatches { + // The negation happens last for parity with other common calculators. + negation { + operand { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + } + } + assertThat(expression40).evaluatesToIntegerThat().isEqualTo(-6) + + val expression41 = parseAlgebraicExpression("-2^3(4)") + assertThat(expression41).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression41).evaluatesToIntegerThat().isEqualTo(-32) + + val expression43 = parseAlgebraicExpression("√2^2(3)") + assertThat(expression43).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + assertThat(expression43).evaluatesToIrrationalThat().isWithin(1e-5).of(6.0) + + val expression60 = parseAlgebraicExpression("√(2^2(3))") + assertThat(expression60).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + } + } + assertThat(expression60).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(12.0)) + + val expression42 = parseAlgebraicExpression("-2*-2") + // Note that the following structure is not the same as (-2)*(-2) since unary negation has + // higher precedence than multiplication, so it's first & recurses to include the entire + // multiplication expression. + assertThat(expression42).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + } + } + assertThat(expression42).evaluatesToIntegerThat().isEqualTo(4) + + val expression44 = parseAlgebraicExpression("2(2)") + assertThat(expression44).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + assertThat(expression44).evaluatesToIntegerThat().isEqualTo(4) + + val expression45 = parseAlgebraicExpression("2sqrt(2)") + assertThat(expression45).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression45).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) + + val expression46 = parseAlgebraicExpression("2√2") + assertThat(expression46).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression46).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) + + val expression47 = parseAlgebraicExpression("(2)(2)") + assertThat(expression47).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + assertThat(expression47).evaluatesToIntegerThat().isEqualTo(4) + + val expression48 = parseAlgebraicExpression("sqrt(2)(2)") + assertThat(expression48).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + assertThat(expression48).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) + + val expression49 = parseAlgebraicExpression("sqrt(2)sqrt(2)") + assertThat(expression49).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression49).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) + + val expression50 = parseAlgebraicExpression("√2√2") + assertThat(expression50).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression50).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) + + val expression51 = parseAlgebraicExpression("(2)(2)(2)") + assertThat(expression51).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + assertThat(expression51).evaluatesToIntegerThat().isEqualTo(8) + + val expression52 = parseAlgebraicExpression("sqrt(2)sqrt(2)sqrt(2)") + assertThat(expression52).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + val sqrt2 = sqrt(2.0) + assertThat(expression52).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) + + val expression53 = parseAlgebraicExpression("√2√2√2") + assertThat(expression53).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression53).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) + + // Should fail for algebra. + expectFailureWhenParsingAlgebraicExpression("x7") + + // Should pass for algebra. + val expression67 = parseAlgebraicExpression("2x^2y^-3") + assertThat(expression67).hasStructureThatMatches { + // 2x^2y^-3 -> (2*(x^2))*(y^(-3)) + multiplication { + // 2x^2 + leftOperand { + multiplication { + // 2 + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + // x^2 + rightOperand { + exponentiation { + // x + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + // 2 + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + // y^-3 + rightOperand { + exponentiation { + // y + leftOperand { + variable { + withNameThat().isEqualTo("y") + } + } + // -3 + rightOperand { + negation { + // 3 + operand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + } + } + } + } + } + } + + val expression54 = parseAlgebraicExpression("2*2/-4+7*2") + assertThat(expression54).hasStructureThatMatches { + // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) + addition { + leftOperand { + // 2*2/-4 + division { + leftOperand { + // 2*2 + multiplication { + leftOperand { + // 2 + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + // 2 + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + rightOperand { + // -4 + negation { + // 4 + operand { + constant { + withIntegerValueThat().isEqualTo(4) + } + } + } + } + } + } + rightOperand { + // 7*2 + multiplication { + leftOperand { + // 7 + constant { + withIntegerValueThat().isEqualTo(7) + } + } + rightOperand { + // 2 + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression54).evaluatesToIntegerThat().isEqualTo(13) + + val expression55 = parseAlgebraicExpression("(3/(1-2))") + assertThat(expression55).hasStructureThatMatches { + division { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression55).evaluatesToIntegerThat().isEqualTo(-3) + + val expression56 = parseAlgebraicExpression("(3)/(1-2)") + assertThat(expression56).hasStructureThatMatches { + division { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } + } + rightOperand { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression56).evaluatesToIntegerThat().isEqualTo(-3) + + val expression57 = parseAlgebraicExpression("3/((1-2))") assertThat(expression57).hasStructureThatMatches { division { leftOperand { @@ -1771,6 +3624,9 @@ class NumericExpressionParserTest { fun constant(init: ConstantComparator.() -> Unit): ConstantComparator = ConstantComparator.createFromExpression(expression).also(init) + fun variable(init: VariableComparator.() -> Unit): VariableComparator = + VariableComparator.createFromExpression(expression).also(init) + fun addition(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { return BinaryOperationComparator.createFromExpression( expression, @@ -1856,6 +3712,18 @@ class NumericExpressionParserTest { } } + @ExpressionComparatorMarker + class VariableComparator private constructor(private val variableName: String) { + fun withNameThat(): StringSubject = assertThat(variableName) + + internal companion object { + fun createFromExpression(expression: MathExpression): VariableComparator { + assertThat(expression.expressionTypeCase).isEqualTo(VARIABLE) + return VariableComparator(expression.variable) + } + } + } + @ExpressionComparatorMarker class BinaryOperationComparator private constructor( private val operation: MathBinaryOperation @@ -1940,12 +3808,27 @@ class NumericExpressionParserTest { } private companion object { - private fun expectFailureWhenParsing(expression: String) { - assertThrows(NumericExpressionParser.ParseException::class) { parseExpression(expression) } + private fun expectFailureWhenParsingNumericExpression(expression: String) { + assertThrows(NumericExpressionParser.ParseException::class) { + parseNumericExpression(expression) + } + } + + private fun parseNumericExpression(expression: String): MathExpression { + return NumericExpressionParser.parseNumericExpression(expression) + } + + private fun expectFailureWhenParsingAlgebraicExpression(expression: String) { + assertThrows(NumericExpressionParser.ParseException::class) { + parseAlgebraicExpression(expression) + } } - private fun parseExpression(expression: String): MathExpression { - return NumericExpressionParser(expression).parse() + private fun parseAlgebraicExpression( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + return NumericExpressionParser.parseAlgebraicExpression(expression, allowedVariables) } private fun assertThat(actual: MathExpression): MathExpressionSubject = From 0ddcd2eda73c15993b9c55c3c1cd4ae7136c6f26 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 15 Nov 2021 10:52:38 -0800 Subject: [PATCH 075/289] Proof-of-concept: parser DSL. This commit introduces a proof-of-concept Kotlin DSL to define the numeric expression grammar generically. While it can be simplified some, it doesn't seem like the more complex syntax (which ultimately doesn't look close enough to EBNF to be exactly comparable) and extra infrastructural complexity seems beneficial over the simpler, hand-maintained recursive descent parser. Maybe this could be useful if more languages need to be supported in the future. Note that the NumericExpressionParserTest is failing in this commit due to an infinite loop, so the parser infrastructure is observably broken (but functionally complete). --- .../util/math/NumericExpressionParser.kt | 628 +++++++++++++++++- .../util/math/NumericExpressionParserTest.kt | 2 + 2 files changed, 607 insertions(+), 23 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt index 78ab93f422c..ed9c264dd6d 100644 --- a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt @@ -18,6 +18,10 @@ import org.oppia.android.util.math.MathTokenizer2.Companion.Token.PositiveRealNu import org.oppia.android.util.math.MathTokenizer2.Companion.Token.RightParenthesisSymbol import org.oppia.android.util.math.MathTokenizer2.Companion.Token.SquareRootSymbol import org.oppia.android.util.math.MathTokenizer2.Companion.Token.VariableName +import org.oppia.android.util.math.NumericExpressionParser.Companion.ProductionRuleDefinition.Companion.getFirstAsMatchedRule +import org.oppia.android.util.math.NumericExpressionParser.Companion.ProductionRuleDefinition.Companion.getFirstAsToken +import org.oppia.android.util.math.NumericExpressionParser.Companion.ProductionRuleDefinition.Companion.getMatchedRule +import org.oppia.android.util.math.NumericExpressionParser.Companion.TestClass.ProductionRules.* class NumericExpressionParser private constructor( private val rawExpression: String, @@ -82,7 +86,7 @@ class NumericExpressionParser private constructor( private fun parseGenericAddExpressionRhs(): MathExpression { // generic_add_expression_rhs = plus_operator , generic_mult_div_expression ; - consumeTokenOfType() + tokens.consumeTokenOfType() return parseGenericMultDivExpression() } @@ -90,7 +94,7 @@ class NumericExpressionParser private constructor( private fun parseGenericSubExpressionRhs(): MathExpression { // generic_sub_expression_rhs = minus_operator , generic_mult_div_expression ; - consumeTokenOfType() + tokens.consumeTokenOfType() return parseGenericMultDivExpression() } @@ -134,7 +138,7 @@ class NumericExpressionParser private constructor( private fun parseGenericMultExpressionRhs(): MathExpression { // generic_mult_expression_rhs = multiplication_operator , generic_exp_expression ; - consumeTokenOfType() + tokens.consumeTokenOfType() return parseGenericExpExpression() } @@ -142,7 +146,7 @@ class NumericExpressionParser private constructor( private fun parseGenericDivExpressionRhs(): MathExpression { // generic_div_expression_rhs = division_operator , generic_exp_expression ; - consumeTokenOfType() + tokens.consumeTokenOfType() return parseGenericExpExpression() } @@ -197,7 +201,7 @@ class NumericExpressionParser private constructor( // associativity can be kept via backtracking. private fun parseGenericExpExpressionTail(lhs: MathExpression): MathExpression { // generic_exp_expression_tail = exponentiation_operator , generic_exp_expression ; - consumeTokenOfType() + tokens.consumeTokenOfType() return MathExpression.newBuilder().apply { binaryOperation = MathBinaryOperation.newBuilder().apply { operator = MathBinaryOperation.Operator.EXPONENTIATE @@ -273,14 +277,14 @@ class NumericExpressionParser private constructor( private fun parseGenericFunctionExpression(): MathExpression { // generic_function_expression = function_name , left_paren , generic_expression , right_paren ; return MathExpression.newBuilder().apply { - val functionName = consumeTokenOfType() + val functionName = tokens.consumeTokenOfType() if (functionName.parsedName != "sqrt") throw ParseException() - consumeTokenOfType() + tokens.consumeTokenOfType() functionCall = MathFunctionCall.newBuilder().apply { functionType = MathFunctionCall.FunctionType.SQUARE_ROOT argument = parseGenericExpression() }.build() - consumeTokenOfType() + tokens.consumeTokenOfType() }.build() } @@ -288,9 +292,9 @@ class NumericExpressionParser private constructor( private fun parseGenericGroupExpression(): MathExpression { // generic_group_expression = left_paren , generic_expression , right_paren ; - consumeTokenOfType() + tokens.consumeTokenOfType() return parseGenericExpression().also { - consumeTokenOfType() + tokens.consumeTokenOfType() } } @@ -310,7 +314,7 @@ class NumericExpressionParser private constructor( private fun parseGenericNegatedTerm(): MathExpression { // generic_negated_term = minus_operator , generic_mult_div_expression ; - consumeTokenOfType() + tokens.consumeTokenOfType() return MathExpression.newBuilder().apply { unaryOperation = MathUnaryOperation.newBuilder().apply { operator = MathUnaryOperation.Operator.NEGATE @@ -323,7 +327,7 @@ class NumericExpressionParser private constructor( private fun parseGenericPositiveTerm(): MathExpression { // generic_positive_term = plus_operator , generic_mult_div_expression ; - consumeTokenOfType() + tokens.consumeTokenOfType() return MathExpression.newBuilder().apply { unaryOperation = MathUnaryOperation.newBuilder().apply { operator = MathUnaryOperation.Operator.POSITIVE @@ -336,7 +340,7 @@ class NumericExpressionParser private constructor( private fun parseGenericRootedTerm(): MathExpression { // generic_rooted_term = square_root_operator , generic_term_with_unary ; - consumeTokenOfType() + tokens.consumeTokenOfType() return MathExpression.newBuilder().apply { functionCall = MathFunctionCall.newBuilder().apply { functionType = MathFunctionCall.FunctionType.SQUARE_ROOT @@ -352,10 +356,10 @@ class NumericExpressionParser private constructor( return MathExpression.newBuilder().apply { constant = when { hasNextPositiveInteger() -> Real.newBuilder().apply { - integer = consumeTokenOfType().parsedValue + integer = tokens.consumeTokenOfType().parsedValue }.build() hasNextPositiveRealNumber() -> Real.newBuilder().apply { - irrational = consumeTokenOfType().parsedValue + irrational = tokens.consumeTokenOfType().parsedValue }.build() // TODO: add error that one of the above was expected. Other error handling should maybe // happen in the same way. @@ -371,7 +375,7 @@ class NumericExpressionParser private constructor( private fun hasNextVariable(): Boolean = tokens.peek() is VariableName private fun parseVariable(): MathExpression { - val variableName = consumeTokenOfType() + val variableName = tokens.consumeTokenOfType() if (!parseContext.allowsVariable(variableName.parsedName)) { throw ParseException() } @@ -380,15 +384,11 @@ class NumericExpressionParser private constructor( }.build() } - private inline fun consumeTokenOfType(): T { - return (tokens.expectNextMatches { it is T } as? T) ?: throw ParseException() - } - // TODO: do error handling better than this (& in a way that works better with the types of errors // that we want to show users). class ParseException : Exception() - private sealed class ParseContext { + sealed class ParseContext { abstract fun allowsVariable(variableName: String): Boolean object NumericExpressionContext : ParseContext() { @@ -404,8 +404,10 @@ class NumericExpressionParser private constructor( } companion object { - fun parseNumericExpression(rawExpression: String): MathExpression = - NumericExpressionParser(rawExpression, ParseContext.NumericExpressionContext).parseGeneric() + fun parseNumericExpression(rawExpression: String): MathExpression { +// return NumericExpressionParser(rawExpression, ParseContext.NumericExpressionContext).parseGeneric() + return TestClass.createGrammar().parse(rawExpression, ParseContext.NumericExpressionContext) + } fun parseAlgebraicExpression( rawExpression: String, allowedVariables: List @@ -416,5 +418,585 @@ class NumericExpressionParser private constructor( ) return parser.parseGeneric() } + + interface Grammar { + fun parse(rawExpression: String, parseContext: ParseContext): MathExpression + } + + @DslMarker + private annotation class ProductionRuleMarker + + @ProductionRuleMarker + private class GrammarDefinition> private constructor() { + private val definitions = mutableMapOf>() + + // TODO: factor this into the static func. + fun defineConcatenationRule( + name: T, init: ProductionRuleDefinition.Concatenation.() -> Unit + ) { + verifyRuleNameIsUnused(name) + definitions[name] = ProductionRuleDefinition.Concatenation(name).also(init) + } + + fun defineAlternationRule(name: T, init: ProductionRuleDefinition.Alternation.() -> Unit) { + verifyRuleNameIsUnused(name) + definitions[name] = ProductionRuleDefinition.Alternation(name).also(init) + } + + fun defineSingletonRule(name: T, singletonProducer: () -> T) { + verifyRuleNameIsUnused(name) + definitions[name] = ProductionRuleDefinition.Singleton(name, singletonProducer()) + } + + private fun initializeRoot( + name: T + ): ProductionRuleDefinition.Companion.ProductionRule.NonTerminal { + val rules = definitions.mapValues { (_, definition) -> definition.toProductionRule() } + val rootRule = rules[name] ?: error("No rule defined for name: $name") + rootRule.initialize(rules) + return rootRule + } + + private fun verifyRuleNameIsUnused(name: T) { + check(name !in definitions) { "Production rule with $name already defined" } + } + + companion object { + inline fun > defineGrammar( + rootRuleName: T, + init: GrammarDefinition.() -> Unit + ): Grammar { + val rootRule = GrammarDefinition().also(init).initializeRoot(rootRuleName) + return object : Grammar { + override fun parse(rawExpression: String, parseContext: ParseContext): MathExpression { + val tokens = PeekableIterator.fromSequence(MathTokenizer2.tokenize(rawExpression)) + val results = rootRule.parse(tokens, parseContext) + return rootRule.computeMathExpression(results) + } + } + } + } + } + + class TestClass { + companion object { + fun createGrammar(): Grammar { + return GrammarDefinition.defineGrammar(rootRuleName = numeric_expression_grammar) { + defineSingletonRule(numeric_expression_grammar) { numeric_expression } + + defineSingletonRule(numeric_expression) { numeric_add_sub_expression } + + defineConcatenationRule(numeric_add_sub_expression) { + numeric_mult_div_expression and repeated(numeric_add_sub_expression_rhs) + evaluatesToExpression { results -> + var lastLhs = results.getFirstAsMatchedRule().computeMathExpression() + for (index in 1..results.size) { + val matchedRule = results.getMatchedRule(index) + lastLhs = MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = when (val rule = matchedRule.getChildAsRule(index = 0).ruleName) { + numeric_mult_div_expression -> MathBinaryOperation.Operator.ADD + numeric_add_sub_expression_rhs -> MathBinaryOperation.Operator.SUBTRACT + else -> error("Encountered invalid rule in add/sub exp: $rule") + } + this.operator = operator + leftOperand = lastLhs + rightOperand = matchedRule.computeMathExpression() + }.build() + }.build() + } + return@evaluatesToExpression lastLhs + } + } + + defineAlternationRule(numeric_add_sub_expression_rhs) { + numeric_add_expression_rhs or numeric_sub_expression_rhs + evaluatesToExpression { it.getFirstAsMatchedRule().computeMathExpression() } + } + + defineConcatenationRule(numeric_add_expression_rhs) { + token() and numeric_mult_div_expression + evaluatesToExpression { it.getMatchedRule(index = 1).computeMathExpression() } + } + + defineConcatenationRule(numeric_sub_expression_rhs) { + token() and numeric_mult_div_expression + evaluatesToExpression { it.getMatchedRule(index = 1).computeMathExpression() } + } + + defineConcatenationRule(numeric_mult_div_expression) { + numeric_exp_expression and repeated(numeric_mult_div_expression_rhs) + evaluatesToExpression { results -> + var lastLhs = results.getFirstAsMatchedRule().computeMathExpression() + for (index in 1..results.size) { + val matchedRule = results.getMatchedRule(index) + lastLhs = MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = when (val rule = matchedRule.getChildAsRule(index = 0).ruleName) { + numeric_mult_expression_rhs, numeric_implicit_mult_expression_rhs -> + MathBinaryOperation.Operator.MULTIPLY + numeric_div_expression_rhs -> MathBinaryOperation.Operator.DIVIDE + else -> error("Encountered invalid rule in mult/div exp: $rule") + } + this.operator = operator + leftOperand = lastLhs + rightOperand = matchedRule.computeMathExpression() + }.build() + }.build() + } + return@evaluatesToExpression lastLhs + } + } + + defineAlternationRule(numeric_mult_div_expression_rhs) { + numeric_mult_expression_rhs or + numeric_div_expression_rhs or + numeric_implicit_mult_expression_rhs + evaluatesToExpression { it.getFirstAsMatchedRule().computeMathExpression() } + } + + defineConcatenationRule(numeric_mult_expression_rhs) { + token() and numeric_exp_expression + evaluatesToExpression { it.getMatchedRule(index = 1).computeMathExpression() } + } + + defineConcatenationRule(numeric_div_expression_rhs) { + token() and numeric_exp_expression + evaluatesToExpression { it.getMatchedRule(index = 1).computeMathExpression() } + } + + defineSingletonRule(numeric_implicit_mult_expression_rhs) { + numeric_term_without_unary_without_number + } + + defineConcatenationRule(numeric_exp_expression) { + numeric_term_with_unary and optional(numeric_exp_expression_tail) + evaluatesToExpression { results -> + val possibleLhs = results.getFirstAsMatchedRule().computeMathExpression() + return@evaluatesToExpression if (results.size > 1) { + MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = MathBinaryOperation.Operator.EXPONENTIATE + leftOperand = possibleLhs + rightOperand = results.getMatchedRule(index = 1).computeMathExpression() + }.build() + }.build() + } else possibleLhs + } + } + + defineConcatenationRule(numeric_exp_expression_tail) { + token() and numeric_exp_expression + evaluatesToExpression { it.getMatchedRule(index = 1).computeMathExpression() } + } + + defineAlternationRule(numeric_term_with_unary) { + number or numeric_term_without_unary_without_number or numeric_plus_minus_unary_term + evaluatesToExpression { it.getFirstAsMatchedRule().computeMathExpression() } + } + + defineAlternationRule(numeric_term_without_unary_without_number) { + numeric_function_expression or numeric_group_expression or numeric_rooted_term + evaluatesToExpression { it.getFirstAsMatchedRule().computeMathExpression() } + } + + defineConcatenationRule(numeric_function_expression) { + token() and + token() and + numeric_expression and + token() + evaluatesToExpression { it.getMatchedRule(index = 2).computeMathExpression() } + } + + defineConcatenationRule(numeric_group_expression) { + token() and + numeric_expression and + token() + evaluatesToExpression { it.getMatchedRule(index = 1).computeMathExpression() } + } + + defineAlternationRule(numeric_plus_minus_unary_term) { + numeric_negated_term or numeric_positive_term + evaluatesToExpression { it.getFirstAsMatchedRule().computeMathExpression() } + } + + defineConcatenationRule(numeric_negated_term) { + token() and numeric_mult_div_expression + evaluatesToExpression { it.getMatchedRule(index = 1).computeMathExpression() } + } + + defineConcatenationRule(numeric_positive_term) { + token() and numeric_mult_div_expression + evaluatesToExpression { it.getMatchedRule(index = 1).computeMathExpression() } + } + + defineConcatenationRule(numeric_rooted_term) { + token() and numeric_term_with_unary + evaluatesToExpression { it.getMatchedRule(index = 1).computeMathExpression() } + } + + defineAlternationRule(number) { + token() or token() + + evaluatesToExpression { results -> + MathExpression.newBuilder().apply { + constant = Real.newBuilder().apply { + when (val token = results.getFirstAsToken()) { + is PositiveInteger -> integer = token.parsedValue + is PositiveRealNumber -> irrational = token.parsedValue + else -> error("Encountered invalid token during expression creation: $token") + } + }.build() + }.build() + } + } + } + } + } + + enum class ProductionRules { + numeric_expression_grammar, + numeric_expression, + numeric_add_sub_expression, + numeric_add_sub_expression_rhs, + numeric_add_expression_rhs, + numeric_sub_expression_rhs, + numeric_mult_div_expression, + numeric_mult_div_expression_rhs, + numeric_mult_expression_rhs, + numeric_div_expression_rhs, + numeric_implicit_mult_expression_rhs, + numeric_exp_expression, + numeric_exp_expression_tail, + numeric_term_with_unary, + numeric_term_without_unary_without_number, + numeric_function_expression, + numeric_group_expression, + numeric_plus_minus_unary_term, + numeric_negated_term, + numeric_positive_term, + numeric_rooted_term, + number + } + } + + @ProductionRuleMarker + sealed class ProductionRuleDefinition>(private val name: T) { + protected val rules = mutableListOf>() + private var expressionEvaluator: ExpressionEvaluator? = null + + inline fun token(): ProductionRule = + ProductionRule.Terminal.create() + + fun optional(name: T): ProductionRule = ProductionRule.Optional(name) + + fun repeated(name: T): ProductionRule = ProductionRule.Repeated(name) + + fun evaluatesToExpression(evaluator: ExpressionEvaluator) { + check(expressionEvaluator == null) { "Expected evaluator to not already be defined." } + expressionEvaluator = evaluator + } + + abstract fun toProductionRule(): ProductionRule.NonTerminal + + protected fun verifyHasRules() { + check(rules.isNotEmpty()) { "Expected at least one definition in rule: $name." } + } + + protected fun ensureHasExpressionEvaluator(): ExpressionEvaluator { + return checkNotNull(expressionEvaluator) { + "evaluatesToExpression {} must be set up for rule: $name." + } + } + + class Singleton>( + name: T, private val value: T + ): ProductionRuleDefinition(name) { + override fun toProductionRule(): ProductionRule.NonTerminal = + ProductionRule.NonTerminal.Reference(value) + } + + @ProductionRuleMarker + class Concatenation>( + private val name: T + ) : ProductionRuleDefinition(name) { + infix fun A.and(rhs: T) = this.and(ProductionRule.Singleton(rhs)) + + infix fun A.and(rhs: ProductionRule) { + rules += rhs + } + + override fun toProductionRule(): ProductionRule.NonTerminal { + verifyHasRules() + return ProductionRule.NonTerminal.Concatenation( + name, rules, ensureHasExpressionEvaluator() + ) + } + } + + @ProductionRuleMarker + class Alternation>( + private val name: T + ) : ProductionRuleDefinition(name) { + infix fun A.or(rhs: T) = this.or(ProductionRule.Singleton(rhs)) + + infix fun A.or(rhs: ProductionRule) { + rules += rhs + } + + override fun toProductionRule(): ProductionRule.NonTerminal { + verifyHasRules() + return ProductionRule.NonTerminal.Alternation(name, rules, ensureHasExpressionEvaluator()) + } + } + + companion object { + sealed class ProductionRule> { + abstract fun initialize(rules: Map>) + + abstract fun hasNext(tokens: PeekableIterator, parseContext: ParseContext): Boolean + + // TODO: consider putting the tokens iterator in the context if we go with this impl approach. + abstract fun parse( + tokens: PeekableIterator, + parseContext: ParseContext + ): List> + + class Terminal, V : Token>( + private val checkNextTokenMatchesExpectedType: PeekableIterator.() -> Boolean, + private val consumeToken: PeekableIterator.() -> V + ) : ProductionRule() { + override fun initialize(rules: Map>) { + // Nothing to do. + } + + override fun hasNext(tokens: PeekableIterator, parseContext: ParseContext): Boolean = + tokens.checkNextTokenMatchesExpectedType() + + override fun parse( + tokens: PeekableIterator, + parseContext: ParseContext + ): List> = + listOf(ProductionMatchResult.MatchedToken(tokens.consumeToken())) + + companion object { + inline fun , reified V : Token> create(): Terminal = + Terminal({ peek() is V }, { consumeTokenOfType() }) + } + } + + class Singleton>(private val name: T) : ProductionRule() { + private lateinit var rule: ProductionRule + + override fun initialize(rules: Map>) { + if (!::rule.isInitialized) { + rule = rules.getValue(name) + rule.initialize(rules) + } + } + + override fun hasNext(tokens: PeekableIterator, parseContext: ParseContext): Boolean = + rule.hasNext(tokens, parseContext) + + override fun parse( + tokens: PeekableIterator, + parseContext: ParseContext + ): List> = rule.parse(tokens, parseContext) + } + + class Optional>(private val name: T) : ProductionRule() { + private lateinit var rule: ProductionRule + + override fun initialize(rules: Map>) { + if (!::rule.isInitialized) { + rule = rules.getValue(name) + rule.initialize(rules) + } + } + + override fun hasNext(tokens: PeekableIterator, parseContext: ParseContext): Boolean = + rule.hasNext(tokens, parseContext) + + override fun parse( + tokens: PeekableIterator, + parseContext: ParseContext + ): List> { + // Can be "parsed" even if it's absent (such as in concatenation groups). + return if (hasNext(tokens, parseContext)) { + rule.parse(tokens, parseContext) + } else listOf() + } + } + + class Repeated>(private val name: T) : ProductionRule() { + private lateinit var rule: ProductionRule + + override fun initialize(rules: Map>) { + if (!::rule.isInitialized) { + rule = rules.getValue(name) + rule.initialize(rules) + } + } + + override fun hasNext(tokens: PeekableIterator, parseContext: ParseContext): Boolean = + rule.hasNext(tokens, parseContext) + + override fun parse( + tokens: PeekableIterator, + parseContext: ParseContext + ): List> { + return generateSequence { + if (hasNext(tokens, parseContext)) { + rule.parse(tokens, parseContext) + } else listOf() + }.flatten().toList() + } + } + + sealed class NonTerminal>(): ProductionRule() { + abstract fun computeMathExpression( + results: List> + ): MathExpression + + class Reference>(private val name: T) : NonTerminal() { + private lateinit var rule: NonTerminal + + override fun initialize(rules: Map>) { + if (!::rule.isInitialized) { + rule = rules.getValue(name) as NonTerminal + rule.initialize(rules) + } + } + + override fun hasNext(tokens: PeekableIterator, parseContext: ParseContext): Boolean = + rule.hasNext(tokens, parseContext) + + override fun parse( + tokens: PeekableIterator, + parseContext: ParseContext + ): List> = rule.parse(tokens, parseContext) + + override fun computeMathExpression( + results: List> + ): MathExpression = rule.computeMathExpression(results) + } + + class Concatenation>( + private val name: T, + private val rules: List>, + private val expressionEvaluator: ExpressionEvaluator + ) : NonTerminal() { + private var isInitialized = false + + override fun initialize(rules: Map>) { + if (!isInitialized) { + isInitialized = true + this.rules.forEach { it.initialize(rules) } + } + } + + override fun hasNext(tokens: PeekableIterator, parseContext: ParseContext): Boolean = + rules.first().hasNext(tokens, parseContext) + + override fun parse( + tokens: PeekableIterator, + parseContext: ParseContext + ): List> { + val results = rules.flatMap { it.parse(tokens, parseContext) } + return listOf(ProductionMatchResult.MatchedRule(name, this, results)) + } + + override fun computeMathExpression( + results: List> + ): MathExpression = expressionEvaluator(results) + } + + class Alternation>( + private val name: T, + private val rules: List>, + private val expressionEvaluator: ExpressionEvaluator + ) : NonTerminal() { + private var isInitialized = false + + override fun initialize(rules: Map>) { + if (!isInitialized) { + isInitialized = true + this.rules.forEach { it.initialize(rules) } + } + } + + override fun hasNext(tokens: PeekableIterator, parseContext: ParseContext): Boolean = + rules.any { it.hasNext(tokens, parseContext) } + + override fun parse( + tokens: PeekableIterator, + parseContext: ParseContext + ): List> { + val firstMatchingRule = rules.find { it.hasNext(tokens, parseContext) } + // TODO: add context for the failure for error classification. + val results = firstMatchingRule?.parse(tokens, parseContext) + ?: throw ParseException() + return listOf(ProductionMatchResult.MatchedRule(name, this, results)) + } + + override fun computeMathExpression( + results: List> + ): MathExpression = expressionEvaluator(results) + } + } + } + + sealed class ProductionMatchResult> { + abstract val ruleName: T? + + class MatchedToken>(val token: Token): ProductionMatchResult() { + override val ruleName: T? = null + } + + class MatchedRule>( + override val ruleName: T, + private val parent: ProductionRule.NonTerminal, + private val children: List> + ): ProductionMatchResult() { + fun computeMathExpression(): MathExpression = parent.computeMathExpression(children) + + fun getChildAsRule(index: Int): MatchedRule { + check(children.size > index) { + "Expected child list to be at least size ${index + 1}" + } + val result = children[index] + return result as? MatchedRule ?: error("Expected MatchedRule type for $result") + } + } + } + + fun > List>.getFirstAsToken(): Token = + getToken(index = 0) + + fun > List>.getFirstAsMatchedRule(): ProductionMatchResult.MatchedRule = + getMatchedRule(index = 0) + + fun > List>.getToken(index: Int): Token = + getResultWithType>(index).token + + fun > List>.getMatchedRule(index: Int): ProductionMatchResult.MatchedRule = + getResultWithType(index) + + private inline fun < + E: Enum, reified T: ProductionMatchResult + > List>.getResultWithType(index: Int): T { + check(size > index) { "Expected result list to be at least size ${index + 1}" } + val result = this[index] + return result as? T ?: error("Expected different type for $result") + } + } + } } } + +// TODO: make this not bad (e.g. by extracting the generic stuff to a separate package/class). +private typealias ExpressionEvaluator = (List>) -> MathExpression + +inline fun PeekableIterator.consumeTokenOfType(): T { + return (expectNextMatches { it is T } as? T) ?: throw NumericExpressionParser.ParseException() +} diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 5c36b3906d6..5bdf4563e4d 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -32,6 +32,8 @@ import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class NumericExpressionParserTest { + // TODO: split into multiple grammar-specific test suites with a simple high-level one for the class itself. + @Test fun testLotsOfCasesForNumericExpression() { // TODO: split this up From eb343c875f6147cf32ec5360881ff7847df76143 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 15 Nov 2021 10:55:09 -0800 Subject: [PATCH 076/289] Revert "Proof-of-concept: parser DSL." This reverts commit 0ddcd2eda73c15993b9c55c3c1cd4ae7136c6f26. --- .../util/math/NumericExpressionParser.kt | 628 +----------------- .../util/math/NumericExpressionParserTest.kt | 2 - 2 files changed, 23 insertions(+), 607 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt index ed9c264dd6d..78ab93f422c 100644 --- a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt @@ -18,10 +18,6 @@ import org.oppia.android.util.math.MathTokenizer2.Companion.Token.PositiveRealNu import org.oppia.android.util.math.MathTokenizer2.Companion.Token.RightParenthesisSymbol import org.oppia.android.util.math.MathTokenizer2.Companion.Token.SquareRootSymbol import org.oppia.android.util.math.MathTokenizer2.Companion.Token.VariableName -import org.oppia.android.util.math.NumericExpressionParser.Companion.ProductionRuleDefinition.Companion.getFirstAsMatchedRule -import org.oppia.android.util.math.NumericExpressionParser.Companion.ProductionRuleDefinition.Companion.getFirstAsToken -import org.oppia.android.util.math.NumericExpressionParser.Companion.ProductionRuleDefinition.Companion.getMatchedRule -import org.oppia.android.util.math.NumericExpressionParser.Companion.TestClass.ProductionRules.* class NumericExpressionParser private constructor( private val rawExpression: String, @@ -86,7 +82,7 @@ class NumericExpressionParser private constructor( private fun parseGenericAddExpressionRhs(): MathExpression { // generic_add_expression_rhs = plus_operator , generic_mult_div_expression ; - tokens.consumeTokenOfType() + consumeTokenOfType() return parseGenericMultDivExpression() } @@ -94,7 +90,7 @@ class NumericExpressionParser private constructor( private fun parseGenericSubExpressionRhs(): MathExpression { // generic_sub_expression_rhs = minus_operator , generic_mult_div_expression ; - tokens.consumeTokenOfType() + consumeTokenOfType() return parseGenericMultDivExpression() } @@ -138,7 +134,7 @@ class NumericExpressionParser private constructor( private fun parseGenericMultExpressionRhs(): MathExpression { // generic_mult_expression_rhs = multiplication_operator , generic_exp_expression ; - tokens.consumeTokenOfType() + consumeTokenOfType() return parseGenericExpExpression() } @@ -146,7 +142,7 @@ class NumericExpressionParser private constructor( private fun parseGenericDivExpressionRhs(): MathExpression { // generic_div_expression_rhs = division_operator , generic_exp_expression ; - tokens.consumeTokenOfType() + consumeTokenOfType() return parseGenericExpExpression() } @@ -201,7 +197,7 @@ class NumericExpressionParser private constructor( // associativity can be kept via backtracking. private fun parseGenericExpExpressionTail(lhs: MathExpression): MathExpression { // generic_exp_expression_tail = exponentiation_operator , generic_exp_expression ; - tokens.consumeTokenOfType() + consumeTokenOfType() return MathExpression.newBuilder().apply { binaryOperation = MathBinaryOperation.newBuilder().apply { operator = MathBinaryOperation.Operator.EXPONENTIATE @@ -277,14 +273,14 @@ class NumericExpressionParser private constructor( private fun parseGenericFunctionExpression(): MathExpression { // generic_function_expression = function_name , left_paren , generic_expression , right_paren ; return MathExpression.newBuilder().apply { - val functionName = tokens.consumeTokenOfType() + val functionName = consumeTokenOfType() if (functionName.parsedName != "sqrt") throw ParseException() - tokens.consumeTokenOfType() + consumeTokenOfType() functionCall = MathFunctionCall.newBuilder().apply { functionType = MathFunctionCall.FunctionType.SQUARE_ROOT argument = parseGenericExpression() }.build() - tokens.consumeTokenOfType() + consumeTokenOfType() }.build() } @@ -292,9 +288,9 @@ class NumericExpressionParser private constructor( private fun parseGenericGroupExpression(): MathExpression { // generic_group_expression = left_paren , generic_expression , right_paren ; - tokens.consumeTokenOfType() + consumeTokenOfType() return parseGenericExpression().also { - tokens.consumeTokenOfType() + consumeTokenOfType() } } @@ -314,7 +310,7 @@ class NumericExpressionParser private constructor( private fun parseGenericNegatedTerm(): MathExpression { // generic_negated_term = minus_operator , generic_mult_div_expression ; - tokens.consumeTokenOfType() + consumeTokenOfType() return MathExpression.newBuilder().apply { unaryOperation = MathUnaryOperation.newBuilder().apply { operator = MathUnaryOperation.Operator.NEGATE @@ -327,7 +323,7 @@ class NumericExpressionParser private constructor( private fun parseGenericPositiveTerm(): MathExpression { // generic_positive_term = plus_operator , generic_mult_div_expression ; - tokens.consumeTokenOfType() + consumeTokenOfType() return MathExpression.newBuilder().apply { unaryOperation = MathUnaryOperation.newBuilder().apply { operator = MathUnaryOperation.Operator.POSITIVE @@ -340,7 +336,7 @@ class NumericExpressionParser private constructor( private fun parseGenericRootedTerm(): MathExpression { // generic_rooted_term = square_root_operator , generic_term_with_unary ; - tokens.consumeTokenOfType() + consumeTokenOfType() return MathExpression.newBuilder().apply { functionCall = MathFunctionCall.newBuilder().apply { functionType = MathFunctionCall.FunctionType.SQUARE_ROOT @@ -356,10 +352,10 @@ class NumericExpressionParser private constructor( return MathExpression.newBuilder().apply { constant = when { hasNextPositiveInteger() -> Real.newBuilder().apply { - integer = tokens.consumeTokenOfType().parsedValue + integer = consumeTokenOfType().parsedValue }.build() hasNextPositiveRealNumber() -> Real.newBuilder().apply { - irrational = tokens.consumeTokenOfType().parsedValue + irrational = consumeTokenOfType().parsedValue }.build() // TODO: add error that one of the above was expected. Other error handling should maybe // happen in the same way. @@ -375,7 +371,7 @@ class NumericExpressionParser private constructor( private fun hasNextVariable(): Boolean = tokens.peek() is VariableName private fun parseVariable(): MathExpression { - val variableName = tokens.consumeTokenOfType() + val variableName = consumeTokenOfType() if (!parseContext.allowsVariable(variableName.parsedName)) { throw ParseException() } @@ -384,11 +380,15 @@ class NumericExpressionParser private constructor( }.build() } + private inline fun consumeTokenOfType(): T { + return (tokens.expectNextMatches { it is T } as? T) ?: throw ParseException() + } + // TODO: do error handling better than this (& in a way that works better with the types of errors // that we want to show users). class ParseException : Exception() - sealed class ParseContext { + private sealed class ParseContext { abstract fun allowsVariable(variableName: String): Boolean object NumericExpressionContext : ParseContext() { @@ -404,10 +404,8 @@ class NumericExpressionParser private constructor( } companion object { - fun parseNumericExpression(rawExpression: String): MathExpression { -// return NumericExpressionParser(rawExpression, ParseContext.NumericExpressionContext).parseGeneric() - return TestClass.createGrammar().parse(rawExpression, ParseContext.NumericExpressionContext) - } + fun parseNumericExpression(rawExpression: String): MathExpression = + NumericExpressionParser(rawExpression, ParseContext.NumericExpressionContext).parseGeneric() fun parseAlgebraicExpression( rawExpression: String, allowedVariables: List @@ -418,585 +416,5 @@ class NumericExpressionParser private constructor( ) return parser.parseGeneric() } - - interface Grammar { - fun parse(rawExpression: String, parseContext: ParseContext): MathExpression - } - - @DslMarker - private annotation class ProductionRuleMarker - - @ProductionRuleMarker - private class GrammarDefinition> private constructor() { - private val definitions = mutableMapOf>() - - // TODO: factor this into the static func. - fun defineConcatenationRule( - name: T, init: ProductionRuleDefinition.Concatenation.() -> Unit - ) { - verifyRuleNameIsUnused(name) - definitions[name] = ProductionRuleDefinition.Concatenation(name).also(init) - } - - fun defineAlternationRule(name: T, init: ProductionRuleDefinition.Alternation.() -> Unit) { - verifyRuleNameIsUnused(name) - definitions[name] = ProductionRuleDefinition.Alternation(name).also(init) - } - - fun defineSingletonRule(name: T, singletonProducer: () -> T) { - verifyRuleNameIsUnused(name) - definitions[name] = ProductionRuleDefinition.Singleton(name, singletonProducer()) - } - - private fun initializeRoot( - name: T - ): ProductionRuleDefinition.Companion.ProductionRule.NonTerminal { - val rules = definitions.mapValues { (_, definition) -> definition.toProductionRule() } - val rootRule = rules[name] ?: error("No rule defined for name: $name") - rootRule.initialize(rules) - return rootRule - } - - private fun verifyRuleNameIsUnused(name: T) { - check(name !in definitions) { "Production rule with $name already defined" } - } - - companion object { - inline fun > defineGrammar( - rootRuleName: T, - init: GrammarDefinition.() -> Unit - ): Grammar { - val rootRule = GrammarDefinition().also(init).initializeRoot(rootRuleName) - return object : Grammar { - override fun parse(rawExpression: String, parseContext: ParseContext): MathExpression { - val tokens = PeekableIterator.fromSequence(MathTokenizer2.tokenize(rawExpression)) - val results = rootRule.parse(tokens, parseContext) - return rootRule.computeMathExpression(results) - } - } - } - } - } - - class TestClass { - companion object { - fun createGrammar(): Grammar { - return GrammarDefinition.defineGrammar(rootRuleName = numeric_expression_grammar) { - defineSingletonRule(numeric_expression_grammar) { numeric_expression } - - defineSingletonRule(numeric_expression) { numeric_add_sub_expression } - - defineConcatenationRule(numeric_add_sub_expression) { - numeric_mult_div_expression and repeated(numeric_add_sub_expression_rhs) - evaluatesToExpression { results -> - var lastLhs = results.getFirstAsMatchedRule().computeMathExpression() - for (index in 1..results.size) { - val matchedRule = results.getMatchedRule(index) - lastLhs = MathExpression.newBuilder().apply { - binaryOperation = MathBinaryOperation.newBuilder().apply { - operator = when (val rule = matchedRule.getChildAsRule(index = 0).ruleName) { - numeric_mult_div_expression -> MathBinaryOperation.Operator.ADD - numeric_add_sub_expression_rhs -> MathBinaryOperation.Operator.SUBTRACT - else -> error("Encountered invalid rule in add/sub exp: $rule") - } - this.operator = operator - leftOperand = lastLhs - rightOperand = matchedRule.computeMathExpression() - }.build() - }.build() - } - return@evaluatesToExpression lastLhs - } - } - - defineAlternationRule(numeric_add_sub_expression_rhs) { - numeric_add_expression_rhs or numeric_sub_expression_rhs - evaluatesToExpression { it.getFirstAsMatchedRule().computeMathExpression() } - } - - defineConcatenationRule(numeric_add_expression_rhs) { - token() and numeric_mult_div_expression - evaluatesToExpression { it.getMatchedRule(index = 1).computeMathExpression() } - } - - defineConcatenationRule(numeric_sub_expression_rhs) { - token() and numeric_mult_div_expression - evaluatesToExpression { it.getMatchedRule(index = 1).computeMathExpression() } - } - - defineConcatenationRule(numeric_mult_div_expression) { - numeric_exp_expression and repeated(numeric_mult_div_expression_rhs) - evaluatesToExpression { results -> - var lastLhs = results.getFirstAsMatchedRule().computeMathExpression() - for (index in 1..results.size) { - val matchedRule = results.getMatchedRule(index) - lastLhs = MathExpression.newBuilder().apply { - binaryOperation = MathBinaryOperation.newBuilder().apply { - operator = when (val rule = matchedRule.getChildAsRule(index = 0).ruleName) { - numeric_mult_expression_rhs, numeric_implicit_mult_expression_rhs -> - MathBinaryOperation.Operator.MULTIPLY - numeric_div_expression_rhs -> MathBinaryOperation.Operator.DIVIDE - else -> error("Encountered invalid rule in mult/div exp: $rule") - } - this.operator = operator - leftOperand = lastLhs - rightOperand = matchedRule.computeMathExpression() - }.build() - }.build() - } - return@evaluatesToExpression lastLhs - } - } - - defineAlternationRule(numeric_mult_div_expression_rhs) { - numeric_mult_expression_rhs or - numeric_div_expression_rhs or - numeric_implicit_mult_expression_rhs - evaluatesToExpression { it.getFirstAsMatchedRule().computeMathExpression() } - } - - defineConcatenationRule(numeric_mult_expression_rhs) { - token() and numeric_exp_expression - evaluatesToExpression { it.getMatchedRule(index = 1).computeMathExpression() } - } - - defineConcatenationRule(numeric_div_expression_rhs) { - token() and numeric_exp_expression - evaluatesToExpression { it.getMatchedRule(index = 1).computeMathExpression() } - } - - defineSingletonRule(numeric_implicit_mult_expression_rhs) { - numeric_term_without_unary_without_number - } - - defineConcatenationRule(numeric_exp_expression) { - numeric_term_with_unary and optional(numeric_exp_expression_tail) - evaluatesToExpression { results -> - val possibleLhs = results.getFirstAsMatchedRule().computeMathExpression() - return@evaluatesToExpression if (results.size > 1) { - MathExpression.newBuilder().apply { - binaryOperation = MathBinaryOperation.newBuilder().apply { - operator = MathBinaryOperation.Operator.EXPONENTIATE - leftOperand = possibleLhs - rightOperand = results.getMatchedRule(index = 1).computeMathExpression() - }.build() - }.build() - } else possibleLhs - } - } - - defineConcatenationRule(numeric_exp_expression_tail) { - token() and numeric_exp_expression - evaluatesToExpression { it.getMatchedRule(index = 1).computeMathExpression() } - } - - defineAlternationRule(numeric_term_with_unary) { - number or numeric_term_without_unary_without_number or numeric_plus_minus_unary_term - evaluatesToExpression { it.getFirstAsMatchedRule().computeMathExpression() } - } - - defineAlternationRule(numeric_term_without_unary_without_number) { - numeric_function_expression or numeric_group_expression or numeric_rooted_term - evaluatesToExpression { it.getFirstAsMatchedRule().computeMathExpression() } - } - - defineConcatenationRule(numeric_function_expression) { - token() and - token() and - numeric_expression and - token() - evaluatesToExpression { it.getMatchedRule(index = 2).computeMathExpression() } - } - - defineConcatenationRule(numeric_group_expression) { - token() and - numeric_expression and - token() - evaluatesToExpression { it.getMatchedRule(index = 1).computeMathExpression() } - } - - defineAlternationRule(numeric_plus_minus_unary_term) { - numeric_negated_term or numeric_positive_term - evaluatesToExpression { it.getFirstAsMatchedRule().computeMathExpression() } - } - - defineConcatenationRule(numeric_negated_term) { - token() and numeric_mult_div_expression - evaluatesToExpression { it.getMatchedRule(index = 1).computeMathExpression() } - } - - defineConcatenationRule(numeric_positive_term) { - token() and numeric_mult_div_expression - evaluatesToExpression { it.getMatchedRule(index = 1).computeMathExpression() } - } - - defineConcatenationRule(numeric_rooted_term) { - token() and numeric_term_with_unary - evaluatesToExpression { it.getMatchedRule(index = 1).computeMathExpression() } - } - - defineAlternationRule(number) { - token() or token() - - evaluatesToExpression { results -> - MathExpression.newBuilder().apply { - constant = Real.newBuilder().apply { - when (val token = results.getFirstAsToken()) { - is PositiveInteger -> integer = token.parsedValue - is PositiveRealNumber -> irrational = token.parsedValue - else -> error("Encountered invalid token during expression creation: $token") - } - }.build() - }.build() - } - } - } - } - } - - enum class ProductionRules { - numeric_expression_grammar, - numeric_expression, - numeric_add_sub_expression, - numeric_add_sub_expression_rhs, - numeric_add_expression_rhs, - numeric_sub_expression_rhs, - numeric_mult_div_expression, - numeric_mult_div_expression_rhs, - numeric_mult_expression_rhs, - numeric_div_expression_rhs, - numeric_implicit_mult_expression_rhs, - numeric_exp_expression, - numeric_exp_expression_tail, - numeric_term_with_unary, - numeric_term_without_unary_without_number, - numeric_function_expression, - numeric_group_expression, - numeric_plus_minus_unary_term, - numeric_negated_term, - numeric_positive_term, - numeric_rooted_term, - number - } - } - - @ProductionRuleMarker - sealed class ProductionRuleDefinition>(private val name: T) { - protected val rules = mutableListOf>() - private var expressionEvaluator: ExpressionEvaluator? = null - - inline fun token(): ProductionRule = - ProductionRule.Terminal.create() - - fun optional(name: T): ProductionRule = ProductionRule.Optional(name) - - fun repeated(name: T): ProductionRule = ProductionRule.Repeated(name) - - fun evaluatesToExpression(evaluator: ExpressionEvaluator) { - check(expressionEvaluator == null) { "Expected evaluator to not already be defined." } - expressionEvaluator = evaluator - } - - abstract fun toProductionRule(): ProductionRule.NonTerminal - - protected fun verifyHasRules() { - check(rules.isNotEmpty()) { "Expected at least one definition in rule: $name." } - } - - protected fun ensureHasExpressionEvaluator(): ExpressionEvaluator { - return checkNotNull(expressionEvaluator) { - "evaluatesToExpression {} must be set up for rule: $name." - } - } - - class Singleton>( - name: T, private val value: T - ): ProductionRuleDefinition(name) { - override fun toProductionRule(): ProductionRule.NonTerminal = - ProductionRule.NonTerminal.Reference(value) - } - - @ProductionRuleMarker - class Concatenation>( - private val name: T - ) : ProductionRuleDefinition(name) { - infix fun A.and(rhs: T) = this.and(ProductionRule.Singleton(rhs)) - - infix fun A.and(rhs: ProductionRule) { - rules += rhs - } - - override fun toProductionRule(): ProductionRule.NonTerminal { - verifyHasRules() - return ProductionRule.NonTerminal.Concatenation( - name, rules, ensureHasExpressionEvaluator() - ) - } - } - - @ProductionRuleMarker - class Alternation>( - private val name: T - ) : ProductionRuleDefinition(name) { - infix fun A.or(rhs: T) = this.or(ProductionRule.Singleton(rhs)) - - infix fun A.or(rhs: ProductionRule) { - rules += rhs - } - - override fun toProductionRule(): ProductionRule.NonTerminal { - verifyHasRules() - return ProductionRule.NonTerminal.Alternation(name, rules, ensureHasExpressionEvaluator()) - } - } - - companion object { - sealed class ProductionRule> { - abstract fun initialize(rules: Map>) - - abstract fun hasNext(tokens: PeekableIterator, parseContext: ParseContext): Boolean - - // TODO: consider putting the tokens iterator in the context if we go with this impl approach. - abstract fun parse( - tokens: PeekableIterator, - parseContext: ParseContext - ): List> - - class Terminal, V : Token>( - private val checkNextTokenMatchesExpectedType: PeekableIterator.() -> Boolean, - private val consumeToken: PeekableIterator.() -> V - ) : ProductionRule() { - override fun initialize(rules: Map>) { - // Nothing to do. - } - - override fun hasNext(tokens: PeekableIterator, parseContext: ParseContext): Boolean = - tokens.checkNextTokenMatchesExpectedType() - - override fun parse( - tokens: PeekableIterator, - parseContext: ParseContext - ): List> = - listOf(ProductionMatchResult.MatchedToken(tokens.consumeToken())) - - companion object { - inline fun , reified V : Token> create(): Terminal = - Terminal({ peek() is V }, { consumeTokenOfType() }) - } - } - - class Singleton>(private val name: T) : ProductionRule() { - private lateinit var rule: ProductionRule - - override fun initialize(rules: Map>) { - if (!::rule.isInitialized) { - rule = rules.getValue(name) - rule.initialize(rules) - } - } - - override fun hasNext(tokens: PeekableIterator, parseContext: ParseContext): Boolean = - rule.hasNext(tokens, parseContext) - - override fun parse( - tokens: PeekableIterator, - parseContext: ParseContext - ): List> = rule.parse(tokens, parseContext) - } - - class Optional>(private val name: T) : ProductionRule() { - private lateinit var rule: ProductionRule - - override fun initialize(rules: Map>) { - if (!::rule.isInitialized) { - rule = rules.getValue(name) - rule.initialize(rules) - } - } - - override fun hasNext(tokens: PeekableIterator, parseContext: ParseContext): Boolean = - rule.hasNext(tokens, parseContext) - - override fun parse( - tokens: PeekableIterator, - parseContext: ParseContext - ): List> { - // Can be "parsed" even if it's absent (such as in concatenation groups). - return if (hasNext(tokens, parseContext)) { - rule.parse(tokens, parseContext) - } else listOf() - } - } - - class Repeated>(private val name: T) : ProductionRule() { - private lateinit var rule: ProductionRule - - override fun initialize(rules: Map>) { - if (!::rule.isInitialized) { - rule = rules.getValue(name) - rule.initialize(rules) - } - } - - override fun hasNext(tokens: PeekableIterator, parseContext: ParseContext): Boolean = - rule.hasNext(tokens, parseContext) - - override fun parse( - tokens: PeekableIterator, - parseContext: ParseContext - ): List> { - return generateSequence { - if (hasNext(tokens, parseContext)) { - rule.parse(tokens, parseContext) - } else listOf() - }.flatten().toList() - } - } - - sealed class NonTerminal>(): ProductionRule() { - abstract fun computeMathExpression( - results: List> - ): MathExpression - - class Reference>(private val name: T) : NonTerminal() { - private lateinit var rule: NonTerminal - - override fun initialize(rules: Map>) { - if (!::rule.isInitialized) { - rule = rules.getValue(name) as NonTerminal - rule.initialize(rules) - } - } - - override fun hasNext(tokens: PeekableIterator, parseContext: ParseContext): Boolean = - rule.hasNext(tokens, parseContext) - - override fun parse( - tokens: PeekableIterator, - parseContext: ParseContext - ): List> = rule.parse(tokens, parseContext) - - override fun computeMathExpression( - results: List> - ): MathExpression = rule.computeMathExpression(results) - } - - class Concatenation>( - private val name: T, - private val rules: List>, - private val expressionEvaluator: ExpressionEvaluator - ) : NonTerminal() { - private var isInitialized = false - - override fun initialize(rules: Map>) { - if (!isInitialized) { - isInitialized = true - this.rules.forEach { it.initialize(rules) } - } - } - - override fun hasNext(tokens: PeekableIterator, parseContext: ParseContext): Boolean = - rules.first().hasNext(tokens, parseContext) - - override fun parse( - tokens: PeekableIterator, - parseContext: ParseContext - ): List> { - val results = rules.flatMap { it.parse(tokens, parseContext) } - return listOf(ProductionMatchResult.MatchedRule(name, this, results)) - } - - override fun computeMathExpression( - results: List> - ): MathExpression = expressionEvaluator(results) - } - - class Alternation>( - private val name: T, - private val rules: List>, - private val expressionEvaluator: ExpressionEvaluator - ) : NonTerminal() { - private var isInitialized = false - - override fun initialize(rules: Map>) { - if (!isInitialized) { - isInitialized = true - this.rules.forEach { it.initialize(rules) } - } - } - - override fun hasNext(tokens: PeekableIterator, parseContext: ParseContext): Boolean = - rules.any { it.hasNext(tokens, parseContext) } - - override fun parse( - tokens: PeekableIterator, - parseContext: ParseContext - ): List> { - val firstMatchingRule = rules.find { it.hasNext(tokens, parseContext) } - // TODO: add context for the failure for error classification. - val results = firstMatchingRule?.parse(tokens, parseContext) - ?: throw ParseException() - return listOf(ProductionMatchResult.MatchedRule(name, this, results)) - } - - override fun computeMathExpression( - results: List> - ): MathExpression = expressionEvaluator(results) - } - } - } - - sealed class ProductionMatchResult> { - abstract val ruleName: T? - - class MatchedToken>(val token: Token): ProductionMatchResult() { - override val ruleName: T? = null - } - - class MatchedRule>( - override val ruleName: T, - private val parent: ProductionRule.NonTerminal, - private val children: List> - ): ProductionMatchResult() { - fun computeMathExpression(): MathExpression = parent.computeMathExpression(children) - - fun getChildAsRule(index: Int): MatchedRule { - check(children.size > index) { - "Expected child list to be at least size ${index + 1}" - } - val result = children[index] - return result as? MatchedRule ?: error("Expected MatchedRule type for $result") - } - } - } - - fun > List>.getFirstAsToken(): Token = - getToken(index = 0) - - fun > List>.getFirstAsMatchedRule(): ProductionMatchResult.MatchedRule = - getMatchedRule(index = 0) - - fun > List>.getToken(index: Int): Token = - getResultWithType>(index).token - - fun > List>.getMatchedRule(index: Int): ProductionMatchResult.MatchedRule = - getResultWithType(index) - - private inline fun < - E: Enum, reified T: ProductionMatchResult - > List>.getResultWithType(index: Int): T { - check(size > index) { "Expected result list to be at least size ${index + 1}" } - val result = this[index] - return result as? T ?: error("Expected different type for $result") - } - } - } } } - -// TODO: make this not bad (e.g. by extracting the generic stuff to a separate package/class). -private typealias ExpressionEvaluator = (List>) -> MathExpression - -inline fun PeekableIterator.consumeTokenOfType(): T { - return (expectNextMatches { it is T } as? T) ?: throw NumericExpressionParser.ParseException() -} diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 5bdf4563e4d..5c36b3906d6 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -32,8 +32,6 @@ import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class NumericExpressionParserTest { - // TODO: split into multiple grammar-specific test suites with a simple high-level one for the class itself. - @Test fun testLotsOfCasesForNumericExpression() { // TODO: split this up From 3a6ee0e4576398bb484a6b65e637c8b145090bf6 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 15 Nov 2021 11:49:35 -0800 Subject: [PATCH 077/289] Add support for algebraic equations. This introduces basic tests; more thorough tests should be added later. --- model/src/main/proto/math.proto | 5 + .../util/math/NumericExpressionParser.kt | 74 +++++-- .../util/math/NumericExpressionParserTest.kt | 201 ++++++++++++++++++ 3 files changed, 258 insertions(+), 22 deletions(-) diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 3451ad3a48a..655989269ca 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -25,6 +25,11 @@ message MathExpression { } } +message MathEquation { + MathExpression left_side = 1; + MathExpression right_side = 2; +} + message MathBinaryOperation { enum Operator { OPERATOR_UNSPECIFIED = 0; diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt index 78ab93f422c..a9dc422aab5 100644 --- a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt @@ -1,12 +1,14 @@ package org.oppia.android.util.math import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression import org.oppia.android.app.model.MathFunctionCall import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.Real import org.oppia.android.util.math.MathTokenizer2.Companion.Token import org.oppia.android.util.math.MathTokenizer2.Companion.Token.DivideSymbol +import org.oppia.android.util.math.MathTokenizer2.Companion.Token.EqualsSymbol import org.oppia.android.util.math.MathTokenizer2.Companion.Token.ExponentiationSymbol import org.oppia.android.util.math.MathTokenizer2.Companion.Token.FunctionName import org.oppia.android.util.math.MathTokenizer2.Companion.Token.LeftParenthesisSymbol @@ -18,6 +20,8 @@ import org.oppia.android.util.math.MathTokenizer2.Companion.Token.PositiveRealNu import org.oppia.android.util.math.MathTokenizer2.Companion.Token.RightParenthesisSymbol import org.oppia.android.util.math.MathTokenizer2.Companion.Token.SquareRootSymbol import org.oppia.android.util.math.MathTokenizer2.Companion.Token.VariableName +import org.oppia.android.util.math.NumericExpressionParser.ParseContext.AlgebraicExpressionContext +import org.oppia.android.util.math.NumericExpressionParser.ParseContext.NumericExpressionContext class NumericExpressionParser private constructor( private val rawExpression: String, @@ -35,12 +39,31 @@ class NumericExpressionParser private constructor( // TODO: document that 'generic' means either 'numeric' or 'algebraic' (ie that the expression is syntactically the same between both grammars). - private fun parseGeneric(): MathExpression { - return parseGenericExpression().also { - // Make sure all tokens were consumed (otherwise there are trailing tokens which invalidate - // the whole expression). - if (tokens.hasNext()) throw ParseException() - } + private fun parseGenericEquationGrammar(): MathEquation { + // generic_equation_grammar = generic_equation ; + return parseGenericEquation().also { ensureNoRemainingTokens() } + } + + private fun parseGenericExpressionGrammar(): MathExpression { + // generic_expression_grammar = generic_expression ; + return parseGenericExpression().also { ensureNoRemainingTokens() } + } + + private fun ensureNoRemainingTokens() { + // Make sure all tokens were consumed (otherwise there are trailing tokens which invalidate the + // whole grammar). + if (tokens.hasNext()) throw ParseException() + } + + private fun parseGenericEquation(): MathEquation { + // algebraic_equation = generic_expression , equals_operator , generic_expression ; + val lhs = parseGenericExpression() + consumeTokenOfType() + val rhs = parseGenericExpression() + return MathEquation.newBuilder().apply { + leftSide = lhs + rightSide = rhs + }.build() } private fun parseGenericExpression(): MathExpression { @@ -148,8 +171,8 @@ class NumericExpressionParser private constructor( private fun hasNextGenericImplicitMultExpressionRhs(): Boolean { return when (parseContext) { - ParseContext.NumericExpressionContext -> hasNextNumericImplicitMultExpressionRhs() - is ParseContext.AlgebraicExpressionContext -> hasNextAlgebraicImplicitMultOrExpExpressionRhs() + NumericExpressionContext -> hasNextNumericImplicitMultExpressionRhs() + is AlgebraicExpressionContext -> hasNextAlgebraicImplicitMultOrExpExpressionRhs() } } @@ -157,8 +180,8 @@ class NumericExpressionParser private constructor( // generic_implicit_mult_expression_rhs is either numeric_implicit_mult_expression_rhs or // algebraic_implicit_mult_or_exp_expression_rhs depending on the current parser context. return when (parseContext) { - ParseContext.NumericExpressionContext -> parseNumericImplicitMultExpressionRhs() - is ParseContext.AlgebraicExpressionContext -> parseAlgebraicImplicitMultOrExpExpressionRhs() + NumericExpressionContext -> parseNumericImplicitMultExpressionRhs() + is AlgebraicExpressionContext -> parseAlgebraicImplicitMultOrExpExpressionRhs() } } @@ -220,8 +243,8 @@ class NumericExpressionParser private constructor( private fun hasNextGenericTermWithoutUnaryWithoutNumber(): Boolean { return when (parseContext) { - ParseContext.NumericExpressionContext -> hasNextNumericTermWithoutUnaryWithoutNumber() - is ParseContext.AlgebraicExpressionContext -> hasNextAlgebraicTermWithoutUnaryWithoutNumber() + NumericExpressionContext -> hasNextNumericTermWithoutUnaryWithoutNumber() + is AlgebraicExpressionContext -> hasNextAlgebraicTermWithoutUnaryWithoutNumber() } } @@ -229,8 +252,8 @@ class NumericExpressionParser private constructor( // generic_term_without_unary_without_number is either numeric_term_without_unary_without_number // or algebraic_term_without_unary_without_number based the current parser context. return when (parseContext) { - ParseContext.NumericExpressionContext -> parseNumericTermWithoutUnaryWithoutNumber() - is ParseContext.AlgebraicExpressionContext -> parseAlgebraicTermWithoutUnaryWithoutNumber() + NumericExpressionContext -> parseNumericTermWithoutUnaryWithoutNumber() + is AlgebraicExpressionContext -> parseAlgebraicTermWithoutUnaryWithoutNumber() } } @@ -405,16 +428,23 @@ class NumericExpressionParser private constructor( companion object { fun parseNumericExpression(rawExpression: String): MathExpression = - NumericExpressionParser(rawExpression, ParseContext.NumericExpressionContext).parseGeneric() + createNumericParser(rawExpression).parseGenericExpressionGrammar() fun parseAlgebraicExpression( rawExpression: String, allowedVariables: List - ): MathExpression { - val parser = - NumericExpressionParser( - rawExpression, ParseContext.AlgebraicExpressionContext(allowedVariables) - ) - return parser.parseGeneric() - } + ): MathExpression = + createAlgebraicParser(rawExpression, allowedVariables).parseGenericExpressionGrammar() + + fun parseAlgebraicEquation( + rawExpression: String, + allowedVariables: List + ): MathEquation = + createAlgebraicParser(rawExpression, allowedVariables).parseGenericEquationGrammar() + + private fun createNumericParser(rawExpression: String) = + NumericExpressionParser(rawExpression, NumericExpressionContext) + + private fun createAlgebraicParser(rawExpression: String, allowedVariables: List) = + NumericExpressionParser(rawExpression, AlgebraicExpressionContext(allowedVariables)) } } diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 5c36b3906d6..5d0d9be81b4 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -26,6 +26,7 @@ import org.oppia.android.app.model.Real import org.oppia.android.testing.assertThrows import org.robolectric.annotation.LooperMode import kotlin.math.sqrt +import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE /** Tests for [MathExpressionParser]. */ @@ -3587,6 +3588,181 @@ class NumericExpressionParserTest { // tokenizer. } + @Test + fun testLotsOfCasesForAlgebraicEquation() { + expectFailureWhenParsingAlgebraicEquation(" x =") + expectFailureWhenParsingAlgebraicEquation(" = y") + + val equation1 = parseAlgebraicEquation("x = 1") + assertThat(equation1).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("x") + } + } + assertThat(equation1).hasRightHandSideThat().evaluatesToIntegerThat().isEqualTo(1) + + val equation2 = + parseAlgebraicEquation("y = mx + b", allowedVariables = listOf("x", "y", "b", "m")) + assertThat(equation2).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + assertThat(equation2).hasRightHandSideThat().hasStructureThatMatches { + addition { + leftOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("m") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + rightOperand { + variable { + withNameThat().isEqualTo("b") + } + } + } + } + + val equation3 = parseAlgebraicEquation("y = (x+1)^2") + assertThat(equation3).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + assertThat(equation3).hasRightHandSideThat().hasStructureThatMatches { + exponentiation { + leftOperand { + addition { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + + val equation4 = parseAlgebraicEquation("y = (x+1)(x-1)") + assertThat(equation4).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + assertThat(equation4).hasRightHandSideThat().hasStructureThatMatches { + multiplication { + leftOperand { + addition { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + } + } + rightOperand { + subtraction { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + } + } + } + } + + expectFailureWhenParsingAlgebraicEquation("y = (x+1)(x-1) 2") + expectFailureWhenParsingAlgebraicEquation("y 2 = (x+1)(x-1)") + + val equation5 = + parseAlgebraicEquation("a*x^2 + b*x + c = 0", allowedVariables = listOf("x", "a", "b", "c")) + assertThat(equation5).hasLeftHandSideThat().hasStructureThatMatches { + addition { + leftOperand { + addition { + leftOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("a") + } + } + rightOperand { + exponentiation { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("b") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + } + } + rightOperand { + variable { + withNameThat().isEqualTo("c") + } + } + } + } + assertThat(equation5).hasRightHandSideThat().hasStructureThatMatches { + constant { + withIntegerValueThat().isEqualTo(0) + } + } + } + @DslMarker private annotation class ExpressionComparatorMarker @@ -3791,6 +3967,15 @@ class NumericExpressionParserTest { } } + private class MathEquationSubject( + metadata: FailureMetadata, + private val actual: MathEquation + ) : Subject(metadata, actual) { + fun hasLeftHandSideThat(): MathExpressionSubject = assertThat(actual.leftSide) + + fun hasRightHandSideThat(): MathExpressionSubject = assertThat(actual.rightSide) + } + // TODO: move this to a common location. private class FractionSubject( metadata: FailureMetadata, @@ -3831,9 +4016,25 @@ class NumericExpressionParserTest { return NumericExpressionParser.parseAlgebraicExpression(expression, allowedVariables) } + private fun parseAlgebraicEquation( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathEquation { + return NumericExpressionParser.parseAlgebraicEquation(expression, allowedVariables) + } + + private fun expectFailureWhenParsingAlgebraicEquation(expression: String) { + assertThrows(NumericExpressionParser.ParseException::class) { + parseAlgebraicEquation(expression) + } + } + private fun assertThat(actual: MathExpression): MathExpressionSubject = assertAbout(::MathExpressionSubject).that(actual) + private fun assertThat(actual: MathEquation): MathEquationSubject = + assertAbout(::MathEquationSubject).that(actual) + private fun assertThat(actual: Fraction): FractionSubject = assertAbout(::FractionSubject).that(actual) } From b52a03cb24cf888a7c4658ae3a92f92d75e11226 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 16 Nov 2021 21:46:24 -0800 Subject: [PATCH 078/289] Add infrastructural support for errors. --- .../org/oppia/android/util/math/BUILD.bazel | 11 + .../android/util/math/MathParsingError.kt | 67 +++ .../util/math/NumericExpressionParser.kt | 422 ++++++++++++------ .../util/math/NumericExpressionParserTest.kt | 42 +- 4 files changed, 393 insertions(+), 149 deletions(-) create mode 100644 utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index e4c1c1fab8c..de64a550f4f 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -42,6 +42,7 @@ kt_android_library( "//:oppia_testing_visibility", ], deps = [ + ":math_parsing_error", ":peekable_iterator", ":tokenizer", "//model/src/main/proto:math_java_proto_lite", @@ -68,3 +69,13 @@ kt_android_library( "PeekableIterator.kt", ], ) + +kt_android_library( + name = "math_parsing_error", + srcs = [ + "MathParsingError.kt", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + ], +) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt new file mode 100644 index 00000000000..fada0276082 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt @@ -0,0 +1,67 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.Real + +sealed class MathParsingError { + object SpacesBetweenNumbersError : MathParsingError() + + object UnbalancedParenthesesError : MathParsingError() + + data class SingleRedundantParenthesesError( + val rawExpression: String, val expression: MathExpression + ): MathParsingError() + + data class MultipleRedundantParenthesesError( + val rawExpression: String, val expression: MathExpression + ): MathParsingError() + + data class RedundantParenthesesForIndividualTermsError( + val rawExpression: String + ): MathParsingError() + + data class UnnecessarySymbolsError(val invalidCharacter: Char): MathParsingError() + + data class NumberAfterVariableError(val number: Real, val variable: String): MathParsingError() + + data class SubsequentBinaryOperatorsError( + val operator1: String, + val operator2: String + ): MathParsingError() + + object SubsequentUnaryOperatorsError : MathParsingError() + + data class NoVariableOrNumberBeforeBinaryOperatorError( + val operator: MathBinaryOperation.Operator, val operatorSymbol: String + ): MathParsingError() + + data class NoVariableOrNumberAfterBinaryOperatorError( + val operator: MathBinaryOperation.Operator, val operatorSymbol: String + ): MathParsingError() + + object ExponentIsVariableExpressionError : MathParsingError() + + object ExponentTooLargeError : MathParsingError() + + object NestedExponentsError : MathParsingError() + + object HangingSquareRootError : MathParsingError() + + object TermDividedByZeroError : MathParsingError() + + object VariableInNumericExpressionError : MathParsingError() + + data class DisabledVariablesInUseError(val variables: List) : MathParsingError() + + object EquationHasWrongNumberOfEqualsError : MathParsingError() + + object EquationMissingLhsOrRhsError : MathParsingError() + + data class InvalidFunctionInUseError(val functionName: String) : MathParsingError() + + data class FunctionNameUsedAsVariables(val expectedFunctionName: String) : MathParsingError() + + object GenericError : MathParsingError() +} diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt index a9dc422aab5..68cc8fe1921 100644 --- a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt @@ -35,67 +35,73 @@ class NumericExpressionParser private constructor( // - Add helpers to reduce overall parser length. // - Integrate with new errors & update the routines to not rely on exceptions except in actual exceptional cases. Make sure optional errors can be disabled (for testing purposes). // - Rename this to be a generic parser, update the public API, add documentation, remove the old classes, and split up the big test routines into actual separate tests. - // - Add support for equations + tests. + + // TODO: implement specific errors. // TODO: document that 'generic' means either 'numeric' or 'algebraic' (ie that the expression is syntactically the same between both grammars). - private fun parseGenericEquationGrammar(): MathEquation { + private fun parseGenericEquationGrammar(): MathParsingResult { // generic_equation_grammar = generic_equation ; - return parseGenericEquation().also { ensureNoRemainingTokens() } + return parseGenericEquation().maybeFail { ensureNoRemainingTokens() } } - private fun parseGenericExpressionGrammar(): MathExpression { + private fun parseGenericExpressionGrammar(): MathParsingResult { // generic_expression_grammar = generic_expression ; - return parseGenericExpression().also { ensureNoRemainingTokens() } + return parseGenericExpression().maybeFail { ensureNoRemainingTokens() } } - private fun ensureNoRemainingTokens() { + private fun ensureNoRemainingTokens(): MathParsingError? { // Make sure all tokens were consumed (otherwise there are trailing tokens which invalidate the // whole grammar). - if (tokens.hasNext()) throw ParseException() + return if (tokens.hasNext()) { + MathParsingError.GenericError + } else null } - private fun parseGenericEquation(): MathEquation { + private fun parseGenericEquation(): MathParsingResult { // algebraic_equation = generic_expression , equals_operator , generic_expression ; - val lhs = parseGenericExpression() - consumeTokenOfType() - val rhs = parseGenericExpression() - return MathEquation.newBuilder().apply { - leftSide = lhs - rightSide = rhs - }.build() + val lhsResult = parseGenericExpression().also { consumeTokenOfType() } + val rhsResult = lhsResult.flatMap { parseGenericExpression() } + return lhsResult.combineWith(rhsResult) { lhs, rhs -> + MathEquation.newBuilder().apply { + leftSide = lhs + rightSide = rhs + }.build() + } } - private fun parseGenericExpression(): MathExpression { + private fun parseGenericExpression(): MathParsingResult { // generic_expression = generic_add_sub_expression ; return parseGenericAddSubExpression() } // TODO: consider consolidating this with other binary parsing to reduce the overall parser. - private fun parseGenericAddSubExpression(): MathExpression { + private fun parseGenericAddSubExpression(): MathParsingResult { // generic_add_sub_expression = // generic_mult_div_expression , { generic_add_sub_expression_rhs } ; - var lastLhs = parseGenericMultDivExpression() - while (hasNextGenericAddSubExpressionRhs()) { + var lastLhsResult = parseGenericMultDivExpression() + while (!lastLhsResult.isFailure() && hasNextGenericAddSubExpressionRhs()) { // generic_add_sub_expression_rhs = generic_add_expression_rhs | generic_sub_expression_rhs ; - val (operator, rhs) = when { + val (operator, rhsResult) = when { hasNextGenericAddExpressionRhs() -> MathBinaryOperation.Operator.ADD to parseGenericAddExpressionRhs() hasNextGenericSubExpressionRhs() -> MathBinaryOperation.Operator.SUBTRACT to parseGenericSubExpressionRhs() - else -> throw ParseException() + else -> return MathParsingResult.Failure(MathParsingError.GenericError) } - // Compute the next LHS if there is further addition/subtraction. - lastLhs = MathExpression.newBuilder().apply { - binaryOperation = MathBinaryOperation.newBuilder().apply { - this.operator = operator - leftOperand = lastLhs - rightOperand = rhs + lastLhsResult = lastLhsResult.combineWith(rhsResult) { lhs, rhs -> + // Compute the next LHS if there is further addition/subtraction. + MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.newBuilder().apply { + this.operator = operator + leftOperand = lhs + rightOperand = rhs + }.build() }.build() - }.build() + } } - return lastLhs + return lastLhsResult } private fun hasNextGenericAddSubExpressionRhs() = hasNextGenericAddExpressionRhs() @@ -103,49 +109,53 @@ class NumericExpressionParser private constructor( private fun hasNextGenericAddExpressionRhs(): Boolean = tokens.peek() is PlusSymbol - private fun parseGenericAddExpressionRhs(): MathExpression { + private fun parseGenericAddExpressionRhs(): MathParsingResult { // generic_add_expression_rhs = plus_operator , generic_mult_div_expression ; - consumeTokenOfType() - return parseGenericMultDivExpression() + return consumeTokenOfType().flatMap { + parseGenericMultDivExpression() + } } private fun hasNextGenericSubExpressionRhs(): Boolean = tokens.peek() is MinusSymbol - private fun parseGenericSubExpressionRhs(): MathExpression { + private fun parseGenericSubExpressionRhs(): MathParsingResult { // generic_sub_expression_rhs = minus_operator , generic_mult_div_expression ; - consumeTokenOfType() - return parseGenericMultDivExpression() + return consumeTokenOfType().flatMap { + parseGenericMultDivExpression() + } } - private fun parseGenericMultDivExpression(): MathExpression { + private fun parseGenericMultDivExpression(): MathParsingResult { // generic_mult_div_expression = // generic_exp_expression , { generic_mult_div_expression_rhs } ; - var lastLhs = parseGenericExpExpression() - while (hasNextGenericMultDivExpressionRhs()) { + var lastLhsResult = parseGenericExpExpression() + while (!lastLhsResult.isFailure() && hasNextGenericMultDivExpressionRhs()) { // generic_mult_div_expression_rhs = // generic_mult_expression_rhs // | generic_div_expression_rhs // | generic_implicit_mult_expression_rhs ; - val (operator, rhs) = when { + val (operator, rhsResult) = when { hasNextGenericMultExpressionRhs() -> MathBinaryOperation.Operator.MULTIPLY to parseGenericMultExpressionRhs() hasNextGenericDivExpressionRhs() -> MathBinaryOperation.Operator.DIVIDE to parseGenericDivExpressionRhs() hasNextGenericImplicitMultExpressionRhs() -> MathBinaryOperation.Operator.MULTIPLY to parseGenericImplicitMultExpressionRhs() - else -> throw ParseException() + else -> return MathParsingResult.Failure(MathParsingError.GenericError) } // Compute the next LHS if there is further multiplication/division. - lastLhs = MathExpression.newBuilder().apply { - binaryOperation = MathBinaryOperation.newBuilder().apply { - this.operator = operator - leftOperand = lastLhs - rightOperand = rhs + lastLhsResult = lastLhsResult.combineWith(rhsResult) { lhs, rhs -> + MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.newBuilder().apply { + this.operator = operator + leftOperand = lhs + rightOperand = rhs + }.build() }.build() - }.build() + } } - return lastLhs + return lastLhsResult } private fun hasNextGenericMultDivExpressionRhs(): Boolean = @@ -155,18 +165,20 @@ class NumericExpressionParser private constructor( private fun hasNextGenericMultExpressionRhs(): Boolean = tokens.peek() is MultiplySymbol - private fun parseGenericMultExpressionRhs(): MathExpression { + private fun parseGenericMultExpressionRhs(): MathParsingResult { // generic_mult_expression_rhs = multiplication_operator , generic_exp_expression ; - consumeTokenOfType() - return parseGenericExpExpression() + return consumeTokenOfType().flatMap { + parseGenericExpExpression() + } } private fun hasNextGenericDivExpressionRhs(): Boolean = tokens.peek() is DivideSymbol - private fun parseGenericDivExpressionRhs(): MathExpression { + private fun parseGenericDivExpressionRhs(): MathParsingResult { // generic_div_expression_rhs = division_operator , generic_exp_expression ; - consumeTokenOfType() - return parseGenericExpExpression() + return consumeTokenOfType().flatMap { + parseGenericExpExpression() + } } private fun hasNextGenericImplicitMultExpressionRhs(): Boolean { @@ -176,7 +188,7 @@ class NumericExpressionParser private constructor( } } - private fun parseGenericImplicitMultExpressionRhs(): MathExpression { + private fun parseGenericImplicitMultExpressionRhs(): MathParsingResult { // generic_implicit_mult_expression_rhs is either numeric_implicit_mult_expression_rhs or // algebraic_implicit_mult_or_exp_expression_rhs depending on the current parser context. return when (parseContext) { @@ -188,7 +200,7 @@ class NumericExpressionParser private constructor( private fun hasNextNumericImplicitMultExpressionRhs(): Boolean = hasNextGenericTermWithoutUnaryWithoutNumber() - private fun parseNumericImplicitMultExpressionRhs(): MathExpression { + private fun parseNumericImplicitMultExpressionRhs(): MathParsingResult { // numeric_implicit_mult_expression_rhs = generic_term_without_unary_without_number ; return parseGenericTermWithoutUnaryWithoutNumber() } @@ -196,7 +208,7 @@ class NumericExpressionParser private constructor( private fun hasNextAlgebraicImplicitMultOrExpExpressionRhs(): Boolean = hasNextGenericTermWithoutUnaryWithoutNumber() - private fun parseAlgebraicImplicitMultOrExpExpressionRhs(): MathExpression { + private fun parseAlgebraicImplicitMultOrExpExpressionRhs(): MathParsingResult { // algebraic_implicit_mult_or_exp_expression_rhs = // generic_term_without_unary_without_number , [ generic_exp_expression_tail ] ; val possibleLhs = parseGenericTermWithoutUnaryWithoutNumber() @@ -205,7 +217,7 @@ class NumericExpressionParser private constructor( } else possibleLhs } - private fun parseGenericExpExpression(): MathExpression { + private fun parseGenericExpExpression(): MathParsingResult { // generic_exp_expression = generic_term_with_unary , [ generic_exp_expression_tail ] ; val possibleLhs = parseGenericTermWithUnary() return when { @@ -218,26 +230,35 @@ class NumericExpressionParser private constructor( // Use tail recursion so that the last exponentiation is evaluated first, and right-to-left // associativity can be kept via backtracking. - private fun parseGenericExpExpressionTail(lhs: MathExpression): MathExpression { + private fun parseGenericExpExpressionTail( + lhsResult: MathParsingResult + ): MathParsingResult { // generic_exp_expression_tail = exponentiation_operator , generic_exp_expression ; - consumeTokenOfType() - return MathExpression.newBuilder().apply { - binaryOperation = MathBinaryOperation.newBuilder().apply { - operator = MathBinaryOperation.Operator.EXPONENTIATE - leftOperand = lhs - rightOperand = parseGenericExpExpression() + val rhsResult = + lhsResult.flatMap { + consumeTokenOfType() + }.flatMap { + parseGenericExpExpression() + } + return lhsResult.combineWith(rhsResult) { lhs, rhs -> + MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = MathBinaryOperation.Operator.EXPONENTIATE + leftOperand = lhs + rightOperand = rhs + }.build() }.build() - }.build() + } } - private fun parseGenericTermWithUnary(): MathExpression { + private fun parseGenericTermWithUnary(): MathParsingResult { // generic_term_with_unary = // number | generic_term_without_unary_without_number | generic_plus_minus_unary_term ; return when { hasNextGenericPlusMinusUnaryTerm() -> parseGenericPlusMinusUnaryTerm() hasNextNumber() -> parseNumber() hasNextGenericTermWithoutUnaryWithoutNumber() -> parseGenericTermWithoutUnaryWithoutNumber() - else -> throw ParseException() + else -> MathParsingResult.Failure(MathParsingError.GenericError) } } @@ -248,7 +269,7 @@ class NumericExpressionParser private constructor( } } - private fun parseGenericTermWithoutUnaryWithoutNumber(): MathExpression { + private fun parseGenericTermWithoutUnaryWithoutNumber(): MathParsingResult { // generic_term_without_unary_without_number is either numeric_term_without_unary_without_number // or algebraic_term_without_unary_without_number based the current parser context. return when (parseContext) { @@ -262,14 +283,14 @@ class NumericExpressionParser private constructor( || hasNextGenericGroupExpression() || hasNextGenericRootedTerm() - private fun parseNumericTermWithoutUnaryWithoutNumber(): MathExpression { + private fun parseNumericTermWithoutUnaryWithoutNumber(): MathParsingResult { // numeric_term_without_unary_without_number = // generic_function_expression | generic_group_expression | generic_rooted_term ; return when { hasNextGenericFunctionExpression() -> parseGenericFunctionExpression() hasNextGenericGroupExpression() -> parseGenericGroupExpression() hasNextGenericRootedTerm() -> parseGenericRootedTerm() - else -> throw ParseException() + else -> MathParsingResult.Failure(MathParsingError.GenericError) } } @@ -279,7 +300,7 @@ class NumericExpressionParser private constructor( || hasNextGenericRootedTerm() || hasNextVariable() - private fun parseAlgebraicTermWithoutUnaryWithoutNumber(): MathExpression { + private fun parseAlgebraicTermWithoutUnaryWithoutNumber(): MathParsingResult { // algebraic_term_without_unary_without_number = // generic_function_expression | generic_group_expression | generic_rooted_term | variable ; return when { @@ -287,104 +308,123 @@ class NumericExpressionParser private constructor( hasNextGenericGroupExpression() -> parseGenericGroupExpression() hasNextGenericRootedTerm() -> parseGenericRootedTerm() hasNextVariable() -> parseVariable() - else -> throw ParseException() + else -> MathParsingResult.Failure(MathParsingError.GenericError) } } private fun hasNextGenericFunctionExpression(): Boolean = tokens.peek() is FunctionName - private fun parseGenericFunctionExpression(): MathExpression { + private fun parseGenericFunctionExpression(): MathParsingResult { // generic_function_expression = function_name , left_paren , generic_expression , right_paren ; - return MathExpression.newBuilder().apply { - val functionName = consumeTokenOfType() - if (functionName.parsedName != "sqrt") throw ParseException() - consumeTokenOfType() - functionCall = MathFunctionCall.newBuilder().apply { - functionType = MathFunctionCall.FunctionType.SQUARE_ROOT - argument = parseGenericExpression() + val functionNameResult = consumeTokenOfType().maybeFail { functionName -> + if (functionName.parsedName != "sqrt") { + MathParsingError.GenericError + } else null + }.also { consumeTokenOfType() } + val argumentResult = functionNameResult.flatMap { parseGenericExpression() } + return argumentResult.map { arg -> + MathExpression.newBuilder().apply { + functionCall = MathFunctionCall.newBuilder().apply { + functionType = MathFunctionCall.FunctionType.SQUARE_ROOT + argument = arg + }.build() }.build() - consumeTokenOfType() - }.build() + }.also { consumeTokenOfType() } } private fun hasNextGenericGroupExpression(): Boolean = tokens.peek() is LeftParenthesisSymbol - private fun parseGenericGroupExpression(): MathExpression { + private fun parseGenericGroupExpression(): MathParsingResult { // generic_group_expression = left_paren , generic_expression , right_paren ; - consumeTokenOfType() - return parseGenericExpression().also { - consumeTokenOfType() - } + return consumeTokenOfType().flatMap { + parseGenericExpression() + }.also { consumeTokenOfType() } } private fun hasNextGenericPlusMinusUnaryTerm(): Boolean = hasNextGenericNegatedTerm() || hasNextGenericPositiveTerm() - private fun parseGenericPlusMinusUnaryTerm(): MathExpression { + private fun parseGenericPlusMinusUnaryTerm(): MathParsingResult { // generic_plus_minus_unary_term = generic_negated_term | generic_positive_term ; return when { hasNextGenericNegatedTerm() -> parseGenericNegatedTerm() hasNextGenericPositiveTerm() -> parseGenericPositiveTerm() - else -> throw ParseException() + else -> MathParsingResult.Failure(MathParsingError.GenericError) } } private fun hasNextGenericNegatedTerm(): Boolean = tokens.peek() is MinusSymbol - private fun parseGenericNegatedTerm(): MathExpression { + private fun parseGenericNegatedTerm(): MathParsingResult { // generic_negated_term = minus_operator , generic_mult_div_expression ; - consumeTokenOfType() - return MathExpression.newBuilder().apply { - unaryOperation = MathUnaryOperation.newBuilder().apply { - operator = MathUnaryOperation.Operator.NEGATE - operand = parseGenericMultDivExpression() + return consumeTokenOfType().flatMap { + parseGenericMultDivExpression() + }.map { op -> + MathExpression.newBuilder().apply { + unaryOperation = MathUnaryOperation.newBuilder().apply { + operator = MathUnaryOperation.Operator.NEGATE + operand = op + }.build() }.build() - }.build() + } } private fun hasNextGenericPositiveTerm(): Boolean = tokens.peek() is PlusSymbol - private fun parseGenericPositiveTerm(): MathExpression { + private fun parseGenericPositiveTerm(): MathParsingResult { // generic_positive_term = plus_operator , generic_mult_div_expression ; - consumeTokenOfType() - return MathExpression.newBuilder().apply { - unaryOperation = MathUnaryOperation.newBuilder().apply { - operator = MathUnaryOperation.Operator.POSITIVE - operand = parseGenericMultDivExpression() + return consumeTokenOfType().flatMap { parseGenericMultDivExpression() }.map { op -> + MathExpression.newBuilder().apply { + unaryOperation = MathUnaryOperation.newBuilder().apply { + operator = MathUnaryOperation.Operator.POSITIVE + operand = op + }.build() }.build() - }.build() + } } private fun hasNextGenericRootedTerm(): Boolean = tokens.peek() is SquareRootSymbol - private fun parseGenericRootedTerm(): MathExpression { + private fun parseGenericRootedTerm(): MathParsingResult { // generic_rooted_term = square_root_operator , generic_term_with_unary ; consumeTokenOfType() - return MathExpression.newBuilder().apply { - functionCall = MathFunctionCall.newBuilder().apply { - functionType = MathFunctionCall.FunctionType.SQUARE_ROOT - argument = parseGenericTermWithUnary() + return parseGenericTermWithUnary().map { op -> + MathExpression.newBuilder().apply { + functionCall = MathFunctionCall.newBuilder().apply { + functionType = MathFunctionCall.FunctionType.SQUARE_ROOT + argument = op + }.build() }.build() - }.build() + } } private fun hasNextNumber(): Boolean = hasNextPositiveInteger() || hasNextPositiveRealNumber() - private fun parseNumber(): MathExpression { + private fun parseNumber(): MathParsingResult { // number = positive_real_number | positive_integer ; - return MathExpression.newBuilder().apply { - constant = when { - hasNextPositiveInteger() -> Real.newBuilder().apply { - integer = consumeTokenOfType().parsedValue - }.build() - hasNextPositiveRealNumber() -> Real.newBuilder().apply { - irrational = consumeTokenOfType().parsedValue - }.build() - // TODO: add error that one of the above was expected. Other error handling should maybe - // happen in the same way. - else -> throw ParseException() // Something went wrong. + return when { + hasNextPositiveInteger() -> { + consumeTokenOfType().map { int -> + MathExpression.newBuilder().apply { + constant = Real.newBuilder().apply { + integer = int.parsedValue + }.build() + }.build() + } + } + hasNextPositiveRealNumber() -> { + consumeTokenOfType().map { real -> + MathExpression.newBuilder().apply { + constant = Real.newBuilder().apply { + irrational = real.parsedValue + }.build() + }.build() + } } - }.build() + // TODO: add error that one of the above was expected. Other error handling should maybe + // happen in the same way. + else -> MathParsingResult.Failure(MathParsingError.GenericError) + } } private fun hasNextPositiveInteger(): Boolean = tokens.peek() is PositiveInteger @@ -393,24 +433,26 @@ class NumericExpressionParser private constructor( private fun hasNextVariable(): Boolean = tokens.peek() is VariableName - private fun parseVariable(): MathExpression { - val variableName = consumeTokenOfType() - if (!parseContext.allowsVariable(variableName.parsedName)) { - throw ParseException() + private fun parseVariable(): MathParsingResult { + val variableNameResult = consumeTokenOfType().maybeFail { variableName -> + if (!parseContext.allowsVariable(variableName.parsedName)) { + MathParsingError.GenericError + } else null + } + return variableNameResult.map { variableName -> + MathExpression.newBuilder().apply { + variable = variableName.parsedName + }.build() } - return MathExpression.newBuilder().apply { - variable = variableName.parsedName - }.build() } - private inline fun consumeTokenOfType(): T { - return (tokens.expectNextMatches { it is T } as? T) ?: throw ParseException() + private inline fun consumeTokenOfType(): MathParsingResult { + val maybeToken = tokens.expectNextMatches { it is T } as? T + return maybeToken?.let { token -> + MathParsingResult.Success(token) + } ?: MathParsingResult.Failure(MathParsingError.GenericError) } - // TODO: do error handling better than this (& in a way that works better with the types of errors - // that we want to show users). - class ParseException : Exception() - private sealed class ParseContext { abstract fun allowsVariable(variableName: String): Boolean @@ -427,18 +469,24 @@ class NumericExpressionParser private constructor( } companion object { - fun parseNumericExpression(rawExpression: String): MathExpression = + sealed class MathParsingResult { + data class Success(val result: T) : MathParsingResult() + + data class Failure(val error: MathParsingError) : MathParsingResult() + } + + fun parseNumericExpression(rawExpression: String): MathParsingResult = createNumericParser(rawExpression).parseGenericExpressionGrammar() fun parseAlgebraicExpression( rawExpression: String, allowedVariables: List - ): MathExpression = + ): MathParsingResult = createAlgebraicParser(rawExpression, allowedVariables).parseGenericExpressionGrammar() fun parseAlgebraicEquation( rawExpression: String, allowedVariables: List - ): MathEquation = + ): MathParsingResult = createAlgebraicParser(rawExpression, allowedVariables).parseGenericEquationGrammar() private fun createNumericParser(rawExpression: String) = @@ -446,5 +494,103 @@ class NumericExpressionParser private constructor( private fun createAlgebraicParser(rawExpression: String, allowedVariables: List) = NumericExpressionParser(rawExpression, AlgebraicExpressionContext(allowedVariables)) + + private fun MathParsingResult.isFailure() = this is MathParsingResult.Failure + + /** + * Maps [this] result to a new value. Note that this lazily uses the provided function (i.e. + * it's only used if [this] result is passing, otherwise the method will short-circuit a failure + * state so that [this] result's failure is preserved). + * + * @param operation computes a new success result given the current successful result value + * @return a new [MathParsingResult] with a successful result provided by the operation, or the + * preserved failure of [this] result + */ + private fun MathParsingResult.map( + operation: (T1) -> T2 + ): MathParsingResult = flatMap { result -> MathParsingResult.Success(operation(result)) } + + /** + * Maps [this] result to a new value. Note that this lazily uses the provided function (i.e. + * it's only used if [this] result is passing, otherwise the method will short-circuit a failure + * state so that [this] result's failure is preserved). + * + * @param operation computes a new result (either a success or failure) given the current + * successful result value + * @return a new [MathParsingResult] with either a result provided by the operation, or the + * preserved failure of [this] result + */ + private fun MathParsingResult.flatMap( + operation: (T1) -> MathParsingResult + ): MathParsingResult { + return when (this) { + is MathParsingResult.Success -> operation(result) + is MathParsingResult.Failure -> MathParsingResult.Failure(error) + } + } + + /** + * Potentially changes [this] result into a failure based on the provided [operation]. Note that + * this function lazily uses the operation (i.e. it's only called if [this] result is in a + * passing state), and the returned result will only be in a failing state if [operation] + * returns a non-null error. + * + * @param operation computes a failure error, or null if no error was determined, given the + * current successful result value + * @return either [this] or a failing result if [operation] was called & returned a non-null + * error + */ + private fun MathParsingResult.maybeFail( + operation: (T) -> MathParsingError? + ): MathParsingResult = flatMap { result -> + operation(result)?.let { error -> + MathParsingResult.Failure(error) + } ?: this + } + + /** + * Calls an operation if [this] operation isn't already failing, and returns a failure only if + * that operation's result is a failure (otherwise returns [this] result). This function can be + * useful to ensure that subsequent operations are successful even when those operations' + * results are never directly used. + * + * @param operation computes a new result that, when failing, will result in a failing result + * returned from this function. This is only called if [this] result is currently + * successful. + * @return either [this] (iff either this result is failing, or the result of [operation] is a + * success), or the failure returned by [operation] + */ + private fun MathParsingResult.also( + operation: () -> MathParsingResult + ): MathParsingResult = flatMap { + when (val other = operation()) { + is MathParsingResult.Success -> this + is MathParsingResult.Failure -> MathParsingResult.Failure(other.error) + } + } + + /** + * Combines [this] result with another result, given a specific combination function. + * + * @param other the result to combine with [this] result + * @param combine computes a new value given the result from [this] and [other]. Note that this + * is only called if both results are successful, and the corresponding successful values + * are provided in-order ([this] result's value is the first parameter, and [other]'s is the + * second). + * @return either [this] result's or [other]'s failure, if either are failing, or a successful + * result containing the value computed by [combine] + */ + private fun MathParsingResult.combineWith( + other: MathParsingResult, + combine: (I1, I2) -> O, + ): MathParsingResult { + return flatMap { result -> + when (other) { + is MathParsingResult.Success -> + MathParsingResult.Success(combine(result, other.result)) + is MathParsingResult.Failure -> MathParsingResult.Failure(other.error) + } + } + } } } diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 5d0d9be81b4..27c2a6ceb79 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -28,6 +28,7 @@ import org.robolectric.annotation.LooperMode import kotlin.math.sqrt import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.util.math.NumericExpressionParser.Companion.MathParsingResult /** Tests for [MathExpressionParser]. */ @RunWith(AndroidJUnit4::class) @@ -3993,40 +3994,59 @@ class NumericExpressionParserTest { } private companion object { + // TODO: fix helper API. + private fun expectFailureWhenParsingNumericExpression(expression: String) { - assertThrows(NumericExpressionParser.ParseException::class) { - parseNumericExpression(expression) - } + assertThat(parseNumericExpressionInternal(expression)) + .isInstanceOf(MathParsingResult.Failure::class.java) } private fun parseNumericExpression(expression: String): MathExpression { + return (parseNumericExpressionInternal(expression) as MathParsingResult.Success).result + } + + private fun parseNumericExpressionInternal( + expression: String + ): MathParsingResult { return NumericExpressionParser.parseNumericExpression(expression) } private fun expectFailureWhenParsingAlgebraicExpression(expression: String) { - assertThrows(NumericExpressionParser.ParseException::class) { - parseAlgebraicExpression(expression) - } + assertThat(parseAlgebraicExpressionInternal(expression)) + .isInstanceOf(MathParsingResult.Failure::class.java) } private fun parseAlgebraicExpression( expression: String, allowedVariables: List = listOf("x", "y", "z") ): MathExpression { + return (parseAlgebraicExpressionInternal(expression, allowedVariables) as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionInternal( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { return NumericExpressionParser.parseAlgebraicExpression(expression, allowedVariables) } + private fun expectFailureWhenParsingAlgebraicEquation(expression: String) { + assertThat(parseAlgebraicEquationInternal(expression)) + .isInstanceOf(MathParsingResult.Failure::class.java) + } + private fun parseAlgebraicEquation( expression: String, allowedVariables: List = listOf("x", "y", "z") ): MathEquation { - return NumericExpressionParser.parseAlgebraicEquation(expression, allowedVariables) + return (parseAlgebraicEquationInternal(expression, allowedVariables) as MathParsingResult.Success).result } - private fun expectFailureWhenParsingAlgebraicEquation(expression: String) { - assertThrows(NumericExpressionParser.ParseException::class) { - parseAlgebraicEquation(expression) - } + private fun parseAlgebraicEquationInternal( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return NumericExpressionParser.parseAlgebraicEquation(expression, allowedVariables) } private fun assertThat(actual: MathExpression): MathExpressionSubject = From a2ea58df14721d0ec0eb5bad0ab5fdfd12b969ba Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 17 Nov 2021 21:16:56 -0800 Subject: [PATCH 079/289] Add support for some errors. This also adds a lot of new infrastructural support for errors, including tracking groups and token/expression positions within the original raw string. --- model/src/main/proto/math.proto | 16 +- .../util/math/MathExpressionExtensions.kt | 2 + .../android/util/math/MathParsingError.kt | 2 +- .../oppia/android/util/math/MathTokenizer2.kt | 129 +- .../util/math/NumericExpressionParser.kt | 343 +++- .../org/oppia/android/util/math/BUILD.bazel | 1 - .../util/math/NumericExpressionParserTest.kt | 1621 ++++++++++------- 7 files changed, 1318 insertions(+), 796 deletions(-) diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 655989269ca..a8b37c3e877 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -16,12 +16,18 @@ message Fraction { // Represents a mathematical expression such as 1+2. The only expression currently supported is a // binary operation. message MathExpression { + // TODO: document inclusive + int32 parse_start_index = 1; + // TODO: document exclusive + int32 parse_end_index = 2; + oneof expression_type { - Real constant = 1; - string variable = 2; - MathBinaryOperation binary_operation = 3; - MathUnaryOperation unary_operation = 4; - MathFunctionCall function_call = 5; + Real constant = 3; + string variable = 4; + MathBinaryOperation binary_operation = 5; + MathUnaryOperation unary_operation = 6; + MathFunctionCall function_call = 7; + MathExpression group = 8; } } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 599d587d3a2..a78c28e4597 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -22,6 +22,7 @@ import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import kotlin.math.abs import kotlin.math.pow import kotlin.math.sqrt +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP // TODO: split up this extensions file into multiple, clean it up, reorganize, and add tests. @@ -47,6 +48,7 @@ fun MathExpression.evaluate(): Real? { BINARY_OPERATION -> binaryOperation.evaluate() UNARY_OPERATION -> unaryOperation.evaluate() FUNCTION_CALL -> functionCall.evaluate() + GROUP -> group.evaluate() EXPRESSIONTYPE_NOT_SET, null -> null } } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt index fada0276082..dd2c4fc243d 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt @@ -19,7 +19,7 @@ sealed class MathParsingError { ): MathParsingError() data class RedundantParenthesesForIndividualTermsError( - val rawExpression: String + val rawExpression: String, val expression: MathExpression ): MathParsingError() data class UnnecessarySymbolsError(val invalidCharacter: Char): MathParsingError() diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt index 8fefab29949..f3622ed3ff0 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt @@ -22,59 +22,94 @@ class MathTokenizer2 private constructor() { when (chars.peek()) { in '0'..'9' -> tokenizeIntegerOrRealNumber(chars) in 'a'..'z', in 'A'..'Z' -> tokenizeVariableOrFunctionName(chars) - '√' -> tokenizeSymbol(chars) { Token.SquareRootSymbol } - '+' -> tokenizeSymbol(chars) { Token.PlusSymbol } + '√' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.SquareRootSymbol(startIndex, endIndex) + } + '+' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.PlusSymbol(startIndex, endIndex) + } // TODO: add tests for different subtraction/minus symbols. - '-', '−' -> tokenizeSymbol(chars) { Token.MinusSymbol } - '*', '×' -> tokenizeSymbol(chars) { Token.MultiplySymbol } - '/', '÷' -> tokenizeSymbol(chars) { Token.DivideSymbol } - '^' -> tokenizeSymbol(chars) { Token.ExponentiationSymbol } - '=' -> tokenizeSymbol(chars) { Token.EqualsSymbol } - '(' -> tokenizeSymbol(chars) { Token.LeftParenthesisSymbol } - ')' -> tokenizeSymbol(chars) { Token.RightParenthesisSymbol } + '-', '−' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.MinusSymbol(startIndex, endIndex) + } + '*', '×' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.MultiplySymbol(startIndex, endIndex) + } + '/', '÷' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.DivideSymbol(startIndex, endIndex) + } + '^' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.ExponentiationSymbol(startIndex, endIndex) + } + '=' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.EqualsSymbol(startIndex, endIndex) + } + '(' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.LeftParenthesisSymbol(startIndex, endIndex) + } + ')' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.RightParenthesisSymbol(startIndex, endIndex) + } null -> null // End of stream. - else -> { // Invalid character. - chars.next() // Parse the invalid character. - Token.InvalidToken + // Invalid character. + else -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.InvalidToken(startIndex, endIndex) } } } } private fun tokenizeIntegerOrRealNumber(chars: PeekableIterator): Token { - val integerPart1 = parseInteger(chars) ?: return Token.InvalidToken + val startIndex = chars.getRetrievalCount() + val integerPart1 = + parseInteger(chars) + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) chars.consumeWhitespace() // Whitespace is allowed between digits and the '.'. return if (chars.peek() == '.') { chars.next() // Parse the "." since it will be re-added later. chars.consumeWhitespace() // Whitespace is allowed between the '.' and following digits. // Another integer must follow the ".". - val integerPart2 = parseInteger(chars) ?: return Token.InvalidToken + val integerPart2 = parseInteger(chars) + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) // TODO: validate that the result isn't NaN or INF. val doubleValue = "$integerPart1.$integerPart2".toDoubleOrNull() - ?: return Token.InvalidToken - Token.PositiveRealNumber(doubleValue) + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) + Token.PositiveRealNumber(doubleValue, startIndex, endIndex = chars.getRetrievalCount()) } else { - Token.PositiveInteger(integerPart1.toIntOrNull() ?: return Token.InvalidToken) + Token.PositiveInteger( + integerPart1.toIntOrNull() + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()), + startIndex, + endIndex = chars.getRetrievalCount() + ) } } private fun tokenizeVariableOrFunctionName(chars: PeekableIterator): Token { + val startIndex = chars.getRetrievalCount() val firstChar = chars.next() val nextChar = chars.peek() return if (firstChar == 's' && nextChar == 'q') { // With 'sq' next to each other, 'rt' is expected to follow. - chars.expectNextValue { 'q' } ?: return Token.InvalidToken - chars.expectNextValue { 'r' } ?: return Token.InvalidToken - chars.expectNextValue { 't' } ?: return Token.InvalidToken - Token.FunctionName("sqrt") - } else Token.VariableName(firstChar.toString()) + chars.expectNextValue { 'q' } + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) + chars.expectNextValue { 'r' } + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) + chars.expectNextValue { 't' } + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) + Token.FunctionName("sqrt", startIndex, endIndex = chars.getRetrievalCount()) + } else { + Token.VariableName(firstChar.toString(), startIndex, endIndex = chars.getRetrievalCount()) + } } - private fun tokenizeSymbol(chars: PeekableIterator, factory: () -> Token): Token { + private fun tokenizeSymbol(chars: PeekableIterator, factory: (Int, Int) -> Token): Token { + val startIndex = chars.getRetrievalCount() chars.next() // Parse the symbol. - return factory() + val endIndex = chars.getRetrievalCount() + return factory(startIndex, endIndex) } private fun parseInteger(chars: PeekableIterator): String? { @@ -88,34 +123,52 @@ class MathTokenizer2 private constructor() { } sealed class Token { - class PositiveInteger(val parsedValue: Int) : Token() + /** The index in the input stream at which point this token begins. */ + abstract val startIndex: Int + + /** The (exclusive) index in the input stream at which point this token ends. */ + abstract val endIndex: Int + + class PositiveInteger( + val parsedValue: Int, override val startIndex: Int, override val endIndex: Int + ) : Token() - class PositiveRealNumber(val parsedValue: Double) : Token() + class PositiveRealNumber( + val parsedValue: Double, override val startIndex: Int, override val endIndex: Int + ) : Token() - class VariableName(val parsedName: String) : Token() + class VariableName( + val parsedName: String, override val startIndex: Int, override val endIndex: Int + ) : Token() - class FunctionName(val parsedName: String) : Token() + class FunctionName( + val parsedName: String, override val startIndex: Int, override val endIndex: Int + ) : Token() - object MinusSymbol : Token() + class MinusSymbol(override val startIndex: Int, override val endIndex: Int) : Token() - object SquareRootSymbol : Token() + class SquareRootSymbol(override val startIndex: Int, override val endIndex: Int) : Token() - object PlusSymbol : Token() + class PlusSymbol(override val startIndex: Int, override val endIndex: Int) : Token() - object MultiplySymbol : Token() + class MultiplySymbol(override val startIndex: Int, override val endIndex: Int) : Token() - object DivideSymbol : Token() + class DivideSymbol(override val startIndex: Int, override val endIndex: Int) : Token() - object ExponentiationSymbol : Token() + class ExponentiationSymbol(override val startIndex: Int, override val endIndex: Int) : Token() - object EqualsSymbol : Token() + class EqualsSymbol(override val startIndex: Int, override val endIndex: Int) : Token() - object LeftParenthesisSymbol : Token() + class LeftParenthesisSymbol( + override val startIndex: Int, override val endIndex: Int + ) : Token() - object RightParenthesisSymbol : Token() + class RightParenthesisSymbol( + override val startIndex: Int, override val endIndex: Int + ) : Token() // TODO: add context to line & index, and enum for context on failure. - object InvalidToken : Token() + class InvalidToken(override val startIndex: Int, override val endIndex: Int) : Token() } // TODO: consider whether to use the more correct & expensive Java Char.isWhitespace(). diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt index 68cc8fe1921..bd534d63045 100644 --- a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt @@ -3,6 +3,13 @@ package org.oppia.android.util.math import org.oppia.android.app.model.MathBinaryOperation import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.MathFunctionCall import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.Real @@ -37,30 +44,97 @@ class NumericExpressionParser private constructor( // - Rename this to be a generic parser, update the public API, add documentation, remove the old classes, and split up the big test routines into actual separate tests. // TODO: implement specific errors. + // TODO: verify remaining GenericErrors are correct. // TODO: document that 'generic' means either 'numeric' or 'algebraic' (ie that the expression is syntactically the same between both grammars). private fun parseGenericEquationGrammar(): MathParsingResult { // generic_equation_grammar = generic_equation ; - return parseGenericEquation().maybeFail { ensureNoRemainingTokens() } + return parseGenericEquation().maybeFail { equation -> + checkForLearnerErrors(equation.leftSide) ?: checkForLearnerErrors(equation.rightSide) + } } private fun parseGenericExpressionGrammar(): MathParsingResult { // generic_expression_grammar = generic_expression ; - return parseGenericExpression().maybeFail { ensureNoRemainingTokens() } + return parseGenericExpression().maybeFail { expression -> checkForLearnerErrors(expression) } + } + + private fun checkForLearnerErrors(expression: MathExpression): MathParsingError? { + val firstMultiRedundantGroup = expression.findFirstMultiRedundantGroup() + val nextRedundantGroup = expression.findNextRedundantGroup() + // Note that the order of checks here is important since errors have precedence, and some are + // redundant and, in the wrong order, may cause the wrong error to be returned. + val includeOptionalErrors = parseContext.errorCheckingMode.includesOptionalErrors() + return when { + includeOptionalErrors && firstMultiRedundantGroup != null -> { + val subExpression = + parseContext.rawExpression.substring( + firstMultiRedundantGroup.parseStartIndex, firstMultiRedundantGroup.parseEndIndex + ) + MathParsingError.MultipleRedundantParenthesesError(subExpression, firstMultiRedundantGroup) + } + includeOptionalErrors && expression.expressionTypeCase == GROUP -> + MathParsingError.SingleRedundantParenthesesError(parseContext.rawExpression, expression) + includeOptionalErrors && nextRedundantGroup != null -> { + val subExpression = + parseContext.rawExpression.substring( + nextRedundantGroup.parseStartIndex, nextRedundantGroup.parseEndIndex + ) + MathParsingError.RedundantParenthesesForIndividualTermsError( + subExpression, nextRedundantGroup + ) + } + else -> ensureNoRemainingTokens() + } + } + + private fun MathExpression.findFirstMultiRedundantGroup(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findFirstMultiRedundantGroup() + ?: binaryOperation.rightOperand.findFirstMultiRedundantGroup() + } + UNARY_OPERATION -> unaryOperation.operand.findFirstMultiRedundantGroup() + FUNCTION_CALL -> functionCall.argument.findFirstMultiRedundantGroup() + GROUP -> group.takeIf { it.expressionTypeCase == GROUP } ?: group.findFirstMultiRedundantGroup() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextRedundantGroup(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findNextRedundantGroup() + ?: binaryOperation.rightOperand.findNextRedundantGroup() + } + UNARY_OPERATION -> unaryOperation.operand.findNextRedundantGroup() + FUNCTION_CALL -> functionCall.argument.findNextRedundantGroup() + GROUP -> { + group.takeIf { it.expressionTypeCase in listOf(CONSTANT, VARIABLE) } + ?: group.findNextRedundantGroup() + } + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } } private fun ensureNoRemainingTokens(): MathParsingError? { // Make sure all tokens were consumed (otherwise there are trailing tokens which invalidate the // whole grammar). return if (tokens.hasNext()) { - MathParsingError.GenericError + when (tokens.peek()) { + is LeftParenthesisSymbol, is RightParenthesisSymbol -> + MathParsingError.UnbalancedParenthesesError + else -> MathParsingError.GenericError + } } else null } private fun parseGenericEquation(): MathParsingResult { // algebraic_equation = generic_expression , equals_operator , generic_expression ; - val lhsResult = parseGenericExpression().also { consumeTokenOfType() } + val lhsResult = parseGenericExpression().also { + consumeTokenOfType { MathParsingError.GenericError } + } val rhsResult = lhsResult.flatMap { parseGenericExpression() } return lhsResult.combineWith(rhsResult) { lhs, rhs -> MathEquation.newBuilder().apply { @@ -87,12 +161,14 @@ class NumericExpressionParser private constructor( MathBinaryOperation.Operator.ADD to parseGenericAddExpressionRhs() hasNextGenericSubExpressionRhs() -> MathBinaryOperation.Operator.SUBTRACT to parseGenericSubExpressionRhs() - else -> return MathParsingResult.Failure(MathParsingError.GenericError) + else -> return MathParsingError.GenericError.toFailure() } lastLhsResult = lastLhsResult.combineWith(rhsResult) { lhs, rhs -> // Compute the next LHS if there is further addition/subtraction. MathExpression.newBuilder().apply { + parseStartIndex = lhs.parseStartIndex + parseEndIndex = rhs.parseEndIndex binaryOperation = MathBinaryOperation.newBuilder().apply { this.operator = operator leftOperand = lhs @@ -104,14 +180,16 @@ class NumericExpressionParser private constructor( return lastLhsResult } - private fun hasNextGenericAddSubExpressionRhs() = hasNextGenericAddExpressionRhs() - || hasNextGenericSubExpressionRhs() + private fun hasNextGenericAddSubExpressionRhs() = + hasNextGenericAddExpressionRhs() || hasNextGenericSubExpressionRhs() private fun hasNextGenericAddExpressionRhs(): Boolean = tokens.peek() is PlusSymbol private fun parseGenericAddExpressionRhs(): MathParsingResult { // generic_add_expression_rhs = plus_operator , generic_mult_div_expression ; - return consumeTokenOfType().flatMap { + return consumeTokenOfType { + MathParsingError.GenericError + }.flatMap { parseGenericMultDivExpression() } } @@ -120,7 +198,9 @@ class NumericExpressionParser private constructor( private fun parseGenericSubExpressionRhs(): MathParsingResult { // generic_sub_expression_rhs = minus_operator , generic_mult_div_expression ; - return consumeTokenOfType().flatMap { + return consumeTokenOfType { + MathParsingError.GenericError + }.flatMap { parseGenericMultDivExpression() } } @@ -141,12 +221,14 @@ class NumericExpressionParser private constructor( MathBinaryOperation.Operator.DIVIDE to parseGenericDivExpressionRhs() hasNextGenericImplicitMultExpressionRhs() -> MathBinaryOperation.Operator.MULTIPLY to parseGenericImplicitMultExpressionRhs() - else -> return MathParsingResult.Failure(MathParsingError.GenericError) + else -> return MathParsingError.GenericError.toFailure() } // Compute the next LHS if there is further multiplication/division. lastLhsResult = lastLhsResult.combineWith(rhsResult) { lhs, rhs -> MathExpression.newBuilder().apply { + parseStartIndex = lhs.parseStartIndex + parseEndIndex = rhs.parseEndIndex binaryOperation = MathBinaryOperation.newBuilder().apply { this.operator = operator leftOperand = lhs @@ -167,7 +249,9 @@ class NumericExpressionParser private constructor( private fun parseGenericMultExpressionRhs(): MathParsingResult { // generic_mult_expression_rhs = multiplication_operator , generic_exp_expression ; - return consumeTokenOfType().flatMap { + return consumeTokenOfType { + MathParsingError.GenericError + }.flatMap { parseGenericExpExpression() } } @@ -176,14 +260,16 @@ class NumericExpressionParser private constructor( private fun parseGenericDivExpressionRhs(): MathParsingResult { // generic_div_expression_rhs = division_operator , generic_exp_expression ; - return consumeTokenOfType().flatMap { + return consumeTokenOfType { + MathParsingError.GenericError + }.flatMap { parseGenericExpExpression() } } private fun hasNextGenericImplicitMultExpressionRhs(): Boolean { return when (parseContext) { - NumericExpressionContext -> hasNextNumericImplicitMultExpressionRhs() + is NumericExpressionContext -> hasNextNumericImplicitMultExpressionRhs() is AlgebraicExpressionContext -> hasNextAlgebraicImplicitMultOrExpExpressionRhs() } } @@ -192,7 +278,7 @@ class NumericExpressionParser private constructor( // generic_implicit_mult_expression_rhs is either numeric_implicit_mult_expression_rhs or // algebraic_implicit_mult_or_exp_expression_rhs depending on the current parser context. return when (parseContext) { - NumericExpressionContext -> parseNumericImplicitMultExpressionRhs() + is NumericExpressionContext -> parseNumericImplicitMultExpressionRhs() is AlgebraicExpressionContext -> parseAlgebraicImplicitMultOrExpExpressionRhs() } } @@ -236,12 +322,14 @@ class NumericExpressionParser private constructor( // generic_exp_expression_tail = exponentiation_operator , generic_exp_expression ; val rhsResult = lhsResult.flatMap { - consumeTokenOfType() + consumeTokenOfType { MathParsingError.GenericError } }.flatMap { parseGenericExpExpression() } return lhsResult.combineWith(rhsResult) { lhs, rhs -> MathExpression.newBuilder().apply { + parseStartIndex = lhs.parseStartIndex + parseEndIndex = rhs.parseEndIndex binaryOperation = MathBinaryOperation.newBuilder().apply { operator = MathBinaryOperation.Operator.EXPONENTIATE leftOperand = lhs @@ -256,15 +344,17 @@ class NumericExpressionParser private constructor( // number | generic_term_without_unary_without_number | generic_plus_minus_unary_term ; return when { hasNextGenericPlusMinusUnaryTerm() -> parseGenericPlusMinusUnaryTerm() - hasNextNumber() -> parseNumber() + hasNextNumber() -> parseNumber().takeUnless { + hasNextNumber() + } ?: MathParsingError.SpacesBetweenNumbersError.toFailure() hasNextGenericTermWithoutUnaryWithoutNumber() -> parseGenericTermWithoutUnaryWithoutNumber() - else -> MathParsingResult.Failure(MathParsingError.GenericError) + else -> MathParsingError.GenericError.toFailure() } } private fun hasNextGenericTermWithoutUnaryWithoutNumber(): Boolean { return when (parseContext) { - NumericExpressionContext -> hasNextNumericTermWithoutUnaryWithoutNumber() + is NumericExpressionContext -> hasNextNumericTermWithoutUnaryWithoutNumber() is AlgebraicExpressionContext -> hasNextAlgebraicTermWithoutUnaryWithoutNumber() } } @@ -273,7 +363,7 @@ class NumericExpressionParser private constructor( // generic_term_without_unary_without_number is either numeric_term_without_unary_without_number // or algebraic_term_without_unary_without_number based the current parser context. return when (parseContext) { - NumericExpressionContext -> parseNumericTermWithoutUnaryWithoutNumber() + is NumericExpressionContext -> parseNumericTermWithoutUnaryWithoutNumber() is AlgebraicExpressionContext -> parseAlgebraicTermWithoutUnaryWithoutNumber() } } @@ -290,7 +380,7 @@ class NumericExpressionParser private constructor( hasNextGenericFunctionExpression() -> parseGenericFunctionExpression() hasNextGenericGroupExpression() -> parseGenericGroupExpression() hasNextGenericRootedTerm() -> parseGenericRootedTerm() - else -> MathParsingResult.Failure(MathParsingError.GenericError) + else -> MathParsingError.GenericError.toFailure() } } @@ -308,7 +398,7 @@ class NumericExpressionParser private constructor( hasNextGenericGroupExpression() -> parseGenericGroupExpression() hasNextGenericRootedTerm() -> parseGenericRootedTerm() hasNextVariable() -> parseVariable() - else -> MathParsingResult.Failure(MathParsingError.GenericError) + else -> MathParsingError.GenericError.toFailure() } } @@ -316,29 +406,58 @@ class NumericExpressionParser private constructor( private fun parseGenericFunctionExpression(): MathParsingResult { // generic_function_expression = function_name , left_paren , generic_expression , right_paren ; - val functionNameResult = consumeTokenOfType().maybeFail { functionName -> - if (functionName.parsedName != "sqrt") { + val funcNameResult = + consumeTokenOfType { MathParsingError.GenericError - } else null - }.also { consumeTokenOfType() } - val argumentResult = functionNameResult.flatMap { parseGenericExpression() } - return argumentResult.map { arg -> + }.maybeFail { functionName -> + if (functionName.parsedName != "sqrt") { + MathParsingError.GenericError + } else null + }.also { + consumeTokenOfType { MathParsingError.GenericError } + } + val argResult = funcNameResult.flatMap { parseGenericExpression() } + val rightParenResult = + argResult.flatMap { + consumeTokenOfType { MathParsingError.UnbalancedParenthesesError } + } + return funcNameResult.combineWith(argResult, rightParenResult) { funcName, arg, rightParen -> MathExpression.newBuilder().apply { + parseStartIndex = funcName.startIndex + parseEndIndex = rightParen.endIndex functionCall = MathFunctionCall.newBuilder().apply { functionType = MathFunctionCall.FunctionType.SQUARE_ROOT argument = arg }.build() }.build() - }.also { consumeTokenOfType() } + } } private fun hasNextGenericGroupExpression(): Boolean = tokens.peek() is LeftParenthesisSymbol private fun parseGenericGroupExpression(): MathParsingResult { // generic_group_expression = left_paren , generic_expression , right_paren ; - return consumeTokenOfType().flatMap { - parseGenericExpression() - }.also { consumeTokenOfType() } + val leftParenResult = + consumeTokenOfType { + MathParsingError.GenericError + } + val expResult = + leftParenResult.flatMap { + if (tokens.hasNext()) { + parseGenericExpression() + } else MathParsingError.UnbalancedParenthesesError.toFailure() + } + val rightParenResult = + expResult.flatMap { + consumeTokenOfType { MathParsingError.UnbalancedParenthesesError } + } + return leftParenResult.combineWith(expResult, rightParenResult) { leftParen, exp, rightParen -> + MathExpression.newBuilder().apply { + parseStartIndex = leftParen.startIndex + parseEndIndex = rightParen.endIndex + group = exp + }.build() + } } private fun hasNextGenericPlusMinusUnaryTerm(): Boolean = @@ -349,7 +468,7 @@ class NumericExpressionParser private constructor( return when { hasNextGenericNegatedTerm() -> parseGenericNegatedTerm() hasNextGenericPositiveTerm() -> parseGenericPositiveTerm() - else -> MathParsingResult.Failure(MathParsingError.GenericError) + else -> MathParsingError.GenericError.toFailure() } } @@ -357,10 +476,12 @@ class NumericExpressionParser private constructor( private fun parseGenericNegatedTerm(): MathParsingResult { // generic_negated_term = minus_operator , generic_mult_div_expression ; - return consumeTokenOfType().flatMap { - parseGenericMultDivExpression() - }.map { op -> + val minusResult = consumeTokenOfType { MathParsingError.GenericError } + val expResult = minusResult.flatMap { parseGenericMultDivExpression() } + return minusResult.combineWith(expResult) { minus, op -> MathExpression.newBuilder().apply { + parseStartIndex = minus.startIndex + parseEndIndex = op.parseEndIndex unaryOperation = MathUnaryOperation.newBuilder().apply { operator = MathUnaryOperation.Operator.NEGATE operand = op @@ -373,8 +494,12 @@ class NumericExpressionParser private constructor( private fun parseGenericPositiveTerm(): MathParsingResult { // generic_positive_term = plus_operator , generic_mult_div_expression ; - return consumeTokenOfType().flatMap { parseGenericMultDivExpression() }.map { op -> + val plusResult = consumeTokenOfType { MathParsingError.GenericError } + val expResult = plusResult.flatMap { parseGenericMultDivExpression() } + return plusResult.combineWith(expResult) { plus, op -> MathExpression.newBuilder().apply { + parseStartIndex = plus.startIndex + parseEndIndex = op.parseEndIndex unaryOperation = MathUnaryOperation.newBuilder().apply { operator = MathUnaryOperation.Operator.POSITIVE operand = op @@ -387,9 +512,12 @@ class NumericExpressionParser private constructor( private fun parseGenericRootedTerm(): MathParsingResult { // generic_rooted_term = square_root_operator , generic_term_with_unary ; - consumeTokenOfType() - return parseGenericTermWithUnary().map { op -> + val sqrtResult = consumeTokenOfType { MathParsingError.GenericError } + val expResult = sqrtResult.flatMap { parseGenericTermWithUnary() } + return sqrtResult.combineWith(expResult) { sqrtSymbol, op -> MathExpression.newBuilder().apply { + parseStartIndex = sqrtSymbol.startIndex + parseEndIndex = op.parseEndIndex functionCall = MathFunctionCall.newBuilder().apply { functionType = MathFunctionCall.FunctionType.SQUARE_ROOT argument = op @@ -404,8 +532,12 @@ class NumericExpressionParser private constructor( // number = positive_real_number | positive_integer ; return when { hasNextPositiveInteger() -> { - consumeTokenOfType().map { int -> + consumeTokenOfType { + MathParsingError.GenericError + }.map { int -> MathExpression.newBuilder().apply { + parseStartIndex = int.startIndex + parseEndIndex = int.endIndex constant = Real.newBuilder().apply { integer = int.parsedValue }.build() @@ -413,8 +545,12 @@ class NumericExpressionParser private constructor( } } hasNextPositiveRealNumber() -> { - consumeTokenOfType().map { real -> + consumeTokenOfType { + MathParsingError.GenericError + }.map { real -> MathExpression.newBuilder().apply { + parseStartIndex = real.startIndex + parseEndIndex = real.endIndex constant = Real.newBuilder().apply { irrational = real.parsedValue }.build() @@ -423,7 +559,7 @@ class NumericExpressionParser private constructor( } // TODO: add error that one of the above was expected. Other error handling should maybe // happen in the same way. - else -> MathParsingResult.Failure(MathParsingError.GenericError) + else -> MathParsingError.GenericError.toFailure() } } @@ -434,66 +570,111 @@ class NumericExpressionParser private constructor( private fun hasNextVariable(): Boolean = tokens.peek() is VariableName private fun parseVariable(): MathParsingResult { - val variableNameResult = consumeTokenOfType().maybeFail { variableName -> - if (!parseContext.allowsVariable(variableName.parsedName)) { + val variableNameResult = + consumeTokenOfType { MathParsingError.GenericError - } else null - } + }.maybeFail { variableName -> + if (!parseContext.allowsVariable(variableName.parsedName)) { + MathParsingError.GenericError + } else null + } return variableNameResult.map { variableName -> MathExpression.newBuilder().apply { + parseStartIndex = variableName.startIndex + parseEndIndex = variableName.endIndex variable = variableName.parsedName }.build() } } - private inline fun consumeTokenOfType(): MathParsingResult { + private inline fun consumeTokenOfType( + missingError: () -> MathParsingError + ): MathParsingResult { val maybeToken = tokens.expectNextMatches { it is T } as? T return maybeToken?.let { token -> MathParsingResult.Success(token) - } ?: MathParsingResult.Failure(MathParsingError.GenericError) + } ?: missingError().toFailure() } - private sealed class ParseContext { + private sealed class ParseContext(val rawExpression: String) { + abstract val errorCheckingMode: ErrorCheckingMode + abstract fun allowsVariable(variableName: String): Boolean - object NumericExpressionContext : ParseContext() { + class NumericExpressionContext( + rawExpression: String, override val errorCheckingMode: ErrorCheckingMode + ) : ParseContext(rawExpression) { // Numeric expressions never allow variables. override fun allowsVariable(variableName: String): Boolean = false } - data class AlgebraicExpressionContext( - private val allowedVariables: List - ) : ParseContext() { + class AlgebraicExpressionContext( + rawExpression: String, + private val allowedVariables: List, + override val errorCheckingMode: ErrorCheckingMode + ) : ParseContext(rawExpression) { override fun allowsVariable(variableName: String): Boolean = variableName in allowedVariables } } companion object { + enum class ErrorCheckingMode { + REQUIRED_ONLY, + ALL_ERRORS + } + sealed class MathParsingResult { data class Success(val result: T) : MathParsingResult() data class Failure(val error: MathParsingError) : MathParsingResult() } - fun parseNumericExpression(rawExpression: String): MathParsingResult = - createNumericParser(rawExpression).parseGenericExpressionGrammar() + fun parseNumericExpression( + rawExpression: String, errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS + ): MathParsingResult = + createNumericParser(rawExpression, errorCheckingMode).parseGenericExpressionGrammar() fun parseAlgebraicExpression( - rawExpression: String, allowedVariables: List - ): MathParsingResult = - createAlgebraicParser(rawExpression, allowedVariables).parseGenericExpressionGrammar() + rawExpression: String, + allowedVariables: List, + errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS + ): MathParsingResult { + return createAlgebraicParser( + rawExpression, allowedVariables, errorCheckingMode + ).parseGenericExpressionGrammar() + } fun parseAlgebraicEquation( rawExpression: String, - allowedVariables: List - ): MathParsingResult = - createAlgebraicParser(rawExpression, allowedVariables).parseGenericEquationGrammar() + allowedVariables: List, + errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS + ): MathParsingResult { + return createAlgebraicParser( + rawExpression, allowedVariables, errorCheckingMode + ).parseGenericEquationGrammar() + } + + private fun createNumericParser( + rawExpression: String, errorCheckingMode: ErrorCheckingMode + ): NumericExpressionParser { + return NumericExpressionParser( + rawExpression, NumericExpressionContext(rawExpression, errorCheckingMode) + ) + } - private fun createNumericParser(rawExpression: String) = - NumericExpressionParser(rawExpression, NumericExpressionContext) + private fun createAlgebraicParser( + rawExpression: String, allowedVariables: List, errorCheckingMode: ErrorCheckingMode + ): NumericExpressionParser { + return NumericExpressionParser( + rawExpression, + AlgebraicExpressionContext(rawExpression, allowedVariables, errorCheckingMode) + ) + } - private fun createAlgebraicParser(rawExpression: String, allowedVariables: List) = - NumericExpressionParser(rawExpression, AlgebraicExpressionContext(allowedVariables)) + private fun ErrorCheckingMode.includesOptionalErrors() = this == ErrorCheckingMode.ALL_ERRORS + + private fun MathParsingError.toFailure(): MathParsingResult = + MathParsingResult.Failure(this) private fun MathParsingResult.isFailure() = this is MathParsingResult.Failure @@ -525,7 +706,7 @@ class NumericExpressionParser private constructor( ): MathParsingResult { return when (this) { is MathParsingResult.Success -> operation(result) - is MathParsingResult.Failure -> MathParsingResult.Failure(error) + is MathParsingResult.Failure -> error.toFailure() } } @@ -542,11 +723,7 @@ class NumericExpressionParser private constructor( */ private fun MathParsingResult.maybeFail( operation: (T) -> MathParsingError? - ): MathParsingResult = flatMap { result -> - operation(result)?.let { error -> - MathParsingResult.Failure(error) - } ?: this - } + ): MathParsingResult = flatMap { result -> operation(result)?.toFailure() ?: this } /** * Calls an operation if [this] operation isn't already failing, and returns a failure only if @@ -565,7 +742,7 @@ class NumericExpressionParser private constructor( ): MathParsingResult = flatMap { when (val other = operation()) { is MathParsingResult.Success -> this - is MathParsingResult.Failure -> MathParsingResult.Failure(other.error) + is MathParsingResult.Failure -> other.error.toFailure() } } @@ -585,10 +762,26 @@ class NumericExpressionParser private constructor( combine: (I1, I2) -> O, ): MathParsingResult { return flatMap { result -> - when (other) { - is MathParsingResult.Success -> - MathParsingResult.Success(combine(result, other.result)) - is MathParsingResult.Failure -> MathParsingResult.Failure(other.error) + other.map { otherResult -> + combine(result, otherResult) + } + } + } + + /** + * Performs the same operation as the other [combineWith] function, except with three + * [MathParsingResult]s, instead. + */ + private fun MathParsingResult.combineWith( + other1: MathParsingResult, + other2: MathParsingResult, + combine: (I1, I2, I3) -> O, + ): MathParsingResult { + return flatMap { result -> + other1.flatMap { otherResult1 -> + other2.map { otherResult2 -> + combine(result, otherResult1, otherResult2) + } } } } diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 6813f9ce74e..3994fb4faa9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -48,7 +48,6 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", - "//testing:assertion_helpers", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_truth", "//third_party:junit_junit", diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 27c2a6ceb79..23a752194d2 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -23,24 +23,87 @@ import org.oppia.android.app.model.MathFunctionCall import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.Real -import org.oppia.android.testing.assertThrows import org.robolectric.annotation.LooperMode import kotlin.math.sqrt import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError +import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError +import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError +import org.oppia.android.util.math.NumericExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.NumericExpressionParser.Companion.MathParsingResult /** Tests for [MathExpressionParser]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class NumericExpressionParserTest { + @Test + fun testErrorCases() { + val failure1 = expectFailureWhenParsingNumericExpression("73 2") + assertThat(failure1).isEqualTo(SpacesBetweenNumbersError) + + val failure2 = expectFailureWhenParsingNumericExpression("(73") + assertThat(failure2).isEqualTo(UnbalancedParenthesesError) + + val failure3 = expectFailureWhenParsingNumericExpression("73)") + assertThat(failure3).isEqualTo(UnbalancedParenthesesError) + + val failure4 = expectFailureWhenParsingNumericExpression("((73)") + assertThat(failure4).isEqualTo(UnbalancedParenthesesError) + + val failure5 = expectFailureWhenParsingNumericExpression("73 (") + assertThat(failure5).isEqualTo(UnbalancedParenthesesError) + + val failure6 = expectFailureWhenParsingNumericExpression("73 )") + assertThat(failure6).isEqualTo(UnbalancedParenthesesError) + + val failure7 = expectFailureWhenParsingNumericExpression("sqrt(73") + assertThat(failure7).isEqualTo(UnbalancedParenthesesError) + + // TODO: test properties on errors (& add better testing library for errors, or at least helpers). + val failure8 = expectFailureWhenParsingNumericExpression("(7 * 2 + 4)") + assertThat(failure8).isInstanceOf(SingleRedundantParenthesesError::class.java) + + val failure9 = expectFailureWhenParsingNumericExpression("((5 + 4))") + assertThat(failure9).isInstanceOf(MultipleRedundantParenthesesError::class.java) + + val failure13 = expectFailureWhenParsingNumericExpression("(((5 + 4)))") + assertThat(failure13).isInstanceOf(MultipleRedundantParenthesesError::class.java) + + val failure14 = expectFailureWhenParsingNumericExpression("1+((5 + 4))") + assertThat(failure14).isInstanceOf(MultipleRedundantParenthesesError::class.java) + + val failure15 = expectFailureWhenParsingNumericExpression("1+(7*((( 9 + 3) )))") + assertThat(failure15).isInstanceOf(MultipleRedundantParenthesesError::class.java) + assertThat((failure15 as MultipleRedundantParenthesesError).rawExpression) + .isEqualTo("(( 9 + 3) )") + + parseNumericExpressionWithAllErrors("1+(5+4)") + parseNumericExpressionWithAllErrors("(5+4)+1") + + val failure10 = expectFailureWhenParsingNumericExpression("(5) + 4") + assertThat(failure10).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + + val failure11 = expectFailureWhenParsingNumericExpression("5^(2)") + assertThat(failure11).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + assertThat((failure11 as RedundantParenthesesForIndividualTermsError).rawExpression) + .isEqualTo("2") + + val failure12 = expectFailureWhenParsingNumericExpression("sqrt((2))") + assertThat(failure12).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + + // TODO: Other cases: sqrt(, sqrt(), sqrt 2 + } + @Test fun testLotsOfCasesForNumericExpression() { // TODO: split this up // TODO: add log string generation for expressions. expectFailureWhenParsingNumericExpression("") - val expression1 = parseNumericExpression("1") + val expression1 = parseNumericExpressionWithoutOptionalErrors("1") assertThat(expression1).hasStructureThatMatches { constant { withIntegerValueThat().isEqualTo(1) @@ -50,7 +113,7 @@ class NumericExpressionParserTest { expectFailureWhenParsingNumericExpression("x") - val expression2 = parseNumericExpression(" 2 ") + val expression2 = parseNumericExpressionWithoutOptionalErrors(" 2 ") assertThat(expression2).hasStructureThatMatches { constant { withIntegerValueThat().isEqualTo(2) @@ -58,7 +121,7 @@ class NumericExpressionParserTest { } assertThat(expression2).evaluatesToIntegerThat().isEqualTo(2) - val expression3 = parseNumericExpression(" 2.5 ") + val expression3 = parseNumericExpressionWithoutOptionalErrors(" 2.5 ") assertThat(expression3).hasStructureThatMatches { constant { withIrrationalValueThat().isWithin(1e-5).of(2.5) @@ -70,7 +133,7 @@ class NumericExpressionParserTest { expectFailureWhenParsingNumericExpression(" z x ") - val expression4 = parseNumericExpression("2^3^2") + val expression4 = parseNumericExpressionWithoutOptionalErrors("2^3^2") assertThat(expression4).hasStructureThatMatches { exponentiation { leftOperand { @@ -96,19 +159,21 @@ class NumericExpressionParserTest { } assertThat(expression4).evaluatesToIntegerThat().isEqualTo(512) - val expression23 = parseNumericExpression("(2^3)^2") + val expression23 = parseNumericExpressionWithoutOptionalErrors("(2^3)^2") assertThat(expression23).hasStructureThatMatches { exponentiation { leftOperand { - exponentiation { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(3) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } } } } @@ -122,7 +187,7 @@ class NumericExpressionParserTest { } assertThat(expression23).evaluatesToIntegerThat().isEqualTo(64) - val expression24 = parseNumericExpression("512/32/4") + val expression24 = parseNumericExpressionWithoutOptionalErrors("512/32/4") assertThat(expression24).hasStructureThatMatches { division { leftOperand { @@ -148,7 +213,7 @@ class NumericExpressionParserTest { } assertThat(expression24).evaluatesToIntegerThat().isEqualTo(4) - val expression25 = parseNumericExpression("512/(32/4)") + val expression25 = parseNumericExpressionWithoutOptionalErrors("512/(32/4)") assertThat(expression25).hasStructureThatMatches { division { leftOperand { @@ -157,15 +222,17 @@ class NumericExpressionParserTest { } } rightOperand { - division { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(32) + group { + division { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(32) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(4) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } } } } @@ -174,7 +241,7 @@ class NumericExpressionParserTest { } assertThat(expression25).evaluatesToIntegerThat().isEqualTo(64) - val expression5 = parseNumericExpression("sqrt(2)") + val expression5 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)") assertThat(expression5).hasStructureThatMatches { functionCallTo(SQUARE_ROOT) { argument { @@ -190,7 +257,7 @@ class NumericExpressionParserTest { expectFailureWhenParsingNumericExpression("xyz(2)") - val expression6 = parseNumericExpression("732") + val expression6 = parseNumericExpressionWithoutOptionalErrors("732") assertThat(expression6).hasStructureThatMatches { constant { withIntegerValueThat().isEqualTo(732) @@ -198,10 +265,8 @@ class NumericExpressionParserTest { } assertThat(expression6).evaluatesToIntegerThat().isEqualTo(732) - expectFailureWhenParsingNumericExpression("73 2") - // Verify order of operations between higher & lower precedent operators. - val expression32 = parseNumericExpression("3+4^7") + val expression32 = parseNumericExpressionWithoutOptionalErrors("3+4^7") assertThat(expression32).hasStructureThatMatches { addition { leftOperand { @@ -227,7 +292,7 @@ class NumericExpressionParserTest { } assertThat(expression32).evaluatesToIntegerThat().isEqualTo(16387) - val expression7 = parseNumericExpression("3*2-3+4^7*8/3*2+7") + val expression7 = parseNumericExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") assertThat(expression7).hasStructureThatMatches { // To better visualize the precedence & order of operations, see this grouped version: // (((3*2)-3)+((((4^7)*8)/3)*2))+7. @@ -331,33 +396,37 @@ class NumericExpressionParserTest { expectFailureWhenParsingNumericExpression("x = √2 × 7 ÷ 4") - val expression8 = parseNumericExpression("(1+2)(3+4)") + val expression8 = parseNumericExpressionWithoutOptionalErrors("(1+2)(3+4)") assertThat(expression8).hasStructureThatMatches { multiplication { leftOperand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } } rightOperand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(4) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } } } } @@ -369,7 +438,7 @@ class NumericExpressionParserTest { // Right implicit multiplication of numbers isn't allowed. expectFailureWhenParsingNumericExpression("(1+2)2") - val expression10 = parseNumericExpression("2(1+2)") + val expression10 = parseNumericExpressionWithoutOptionalErrors("2(1+2)") assertThat(expression10).hasStructureThatMatches { multiplication { leftOperand { @@ -378,15 +447,17 @@ class NumericExpressionParserTest { } } rightOperand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -398,7 +469,7 @@ class NumericExpressionParserTest { // Right implicit multiplication of numbers isn't allowed. expectFailureWhenParsingNumericExpression("sqrt(2)3") - val expression12 = parseNumericExpression("3sqrt(2)") + val expression12 = parseNumericExpressionWithoutOptionalErrors("3sqrt(2)") assertThat(expression12).hasStructureThatMatches { multiplication { leftOperand { @@ -421,7 +492,7 @@ class NumericExpressionParserTest { expectFailureWhenParsingNumericExpression("xsqrt(2)") - val expression13 = parseNumericExpression("sqrt(2)*(1+2)*(3-2^5)") + val expression13 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)*(1+2)*(3-2^5)") assertThat(expression13).hasStructureThatMatches { multiplication { leftOperand { @@ -436,15 +507,17 @@ class NumericExpressionParserTest { } } rightOperand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -452,22 +525,24 @@ class NumericExpressionParserTest { } } rightOperand { - subtraction { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(5) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(5) + } } } } @@ -478,7 +553,7 @@ class NumericExpressionParserTest { } assertThat(expression13).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) - val expression58 = parseNumericExpression("sqrt(2)(1+2)(3-2^5)") + val expression58 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)(1+2)(3-2^5)") assertThat(expression58).hasStructureThatMatches { multiplication { leftOperand { @@ -493,15 +568,17 @@ class NumericExpressionParserTest { } } rightOperand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -509,22 +586,24 @@ class NumericExpressionParserTest { } } rightOperand { - subtraction { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(5) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(5) + } } } } @@ -535,15 +614,19 @@ class NumericExpressionParserTest { } assertThat(expression58).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) - val expression14 = parseNumericExpression("((3))") + val expression14 = parseNumericExpressionWithoutOptionalErrors("((3))") assertThat(expression14).hasStructureThatMatches { - constant { - withIntegerValueThat().isEqualTo(3) + group { + group { + constant { + withIntegerValueThat().isEqualTo(3) + } + } } } assertThat(expression14).evaluatesToIntegerThat().isEqualTo(3) - val expression15 = parseNumericExpression("++3") + val expression15 = parseNumericExpressionWithoutOptionalErrors("++3") assertThat(expression15).hasStructureThatMatches { positive { operand { @@ -559,7 +642,7 @@ class NumericExpressionParserTest { } assertThat(expression15).evaluatesToIntegerThat().isEqualTo(3) - val expression16 = parseNumericExpression("--4") + val expression16 = parseNumericExpressionWithoutOptionalErrors("--4") assertThat(expression16).hasStructureThatMatches { negation { operand { @@ -575,7 +658,7 @@ class NumericExpressionParserTest { } assertThat(expression16).evaluatesToIntegerThat().isEqualTo(4) - val expression17 = parseNumericExpression("1+-4") + val expression17 = parseNumericExpressionWithoutOptionalErrors("1+-4") assertThat(expression17).hasStructureThatMatches { addition { leftOperand { @@ -596,7 +679,7 @@ class NumericExpressionParserTest { } assertThat(expression17).evaluatesToIntegerThat().isEqualTo(-3) - val expression18 = parseNumericExpression("1++4") + val expression18 = parseNumericExpressionWithoutOptionalErrors("1++4") assertThat(expression18).hasStructureThatMatches { addition { leftOperand { @@ -617,7 +700,7 @@ class NumericExpressionParserTest { } assertThat(expression18).evaluatesToIntegerThat().isEqualTo(5) - val expression19 = parseNumericExpression("1--4") + val expression19 = parseNumericExpressionWithoutOptionalErrors("1--4") assertThat(expression19).hasStructureThatMatches { subtraction { leftOperand { @@ -640,7 +723,7 @@ class NumericExpressionParserTest { expectFailureWhenParsingNumericExpression("1-^-4") - val expression20 = parseNumericExpression("√2 × 7 ÷ 4") + val expression20 = parseNumericExpressionWithoutOptionalErrors("√2 × 7 ÷ 4") assertThat(expression20).hasStructureThatMatches { division { leftOperand { @@ -672,7 +755,7 @@ class NumericExpressionParserTest { expectFailureWhenParsingNumericExpression("1+2 &asdf") - val expression21 = parseNumericExpression("sqrt(2)sqrt(3)sqrt(4)") + val expression21 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)sqrt(3)sqrt(4)") // Note that this tree demonstrates left associativity. assertThat(expression21).hasStructureThatMatches { multiplication { @@ -714,44 +797,48 @@ class NumericExpressionParserTest { .isWithin(1e-5) .of(sqrt(2.0) * sqrt(3.0) * sqrt(4.0)) - val expression22 = parseNumericExpression("(1+2)(3-7^2)(5+-17)") + val expression22 = parseNumericExpressionWithoutOptionalErrors("(1+2)(3-7^2)(5+-17)") assertThat(expression22).hasStructureThatMatches { multiplication { leftOperand { multiplication { leftOperand { // 1+2 - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } } rightOperand { // 3-7^2 - subtraction { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(7) + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(7) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -762,17 +849,19 @@ class NumericExpressionParserTest { } rightOperand { // 5+-17 - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(5) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(5) + } } - } - rightOperand { - negation { - operand { - constant { - withIntegerValueThat().isEqualTo(17) + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(17) + } } } } @@ -783,7 +872,7 @@ class NumericExpressionParserTest { } assertThat(expression22).evaluatesToIntegerThat().isEqualTo(1656) - val expression26 = parseNumericExpression("3^-2") + val expression26 = parseNumericExpressionWithoutOptionalErrors("3^-2") assertThat(expression26).hasStructureThatMatches { exponentiation { leftOperand { @@ -809,21 +898,23 @@ class NumericExpressionParserTest { hasDenominatorThat().isEqualTo(9) } - val expression27 = parseNumericExpression("(3^-2)^(3^-2)") + val expression27 = parseNumericExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") assertThat(expression27).hasStructureThatMatches { exponentiation { leftOperand { - exponentiation { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } } - } - rightOperand { - negation { - operand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -831,17 +922,19 @@ class NumericExpressionParserTest { } } rightOperand { - exponentiation { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } } - } - rightOperand { - negation { - operand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -852,7 +945,7 @@ class NumericExpressionParserTest { } assertThat(expression27).evaluatesToIrrationalThat().isWithin(1e-5).of(0.78338103693) - val expression28 = parseNumericExpression("1-3^sqrt(4)") + val expression28 = parseNumericExpressionWithoutOptionalErrors("1-3^sqrt(4)") assertThat(expression28).hasStructureThatMatches { subtraction { leftOperand { @@ -884,7 +977,7 @@ class NumericExpressionParserTest { // "Hard" order of operation problems loosely based on & other problems that can often stump // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. - val expression29 = parseNumericExpression("3÷2*(3+4)") + val expression29 = parseNumericExpressionWithoutOptionalErrors("3÷2*(3+4)") assertThat(expression29).hasStructureThatMatches { multiplication { leftOperand { @@ -902,15 +995,17 @@ class NumericExpressionParserTest { } } rightOperand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(4) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } } } } @@ -919,7 +1014,7 @@ class NumericExpressionParserTest { } assertThat(expression29).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) - val expression59 = parseNumericExpression("3÷2(3+4)") + val expression59 = parseNumericExpressionWithoutOptionalErrors("3÷2(3+4)") assertThat(expression59).hasStructureThatMatches { multiplication { leftOperand { @@ -937,15 +1032,17 @@ class NumericExpressionParserTest { } } rightOperand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(4) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } } } } @@ -961,33 +1058,39 @@ class NumericExpressionParserTest { expectFailureWhenParsingNumericExpression("2^2 2") - val expression31 = parseNumericExpression("(3)(4)(5)") + val expression31 = parseNumericExpressionWithoutOptionalErrors("(3)(4)(5)") assertThat(expression31).hasStructureThatMatches { multiplication { leftOperand { multiplication { leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + constant { + withIntegerValueThat().isEqualTo(3) + } } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(4) + group { + constant { + withIntegerValueThat().isEqualTo(4) + } } } } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(5) + group { + constant { + withIntegerValueThat().isEqualTo(5) + } } } } } assertThat(expression31).evaluatesToIntegerThat().isEqualTo(60) - val expression33 = parseNumericExpression("2^(3)") + val expression33 = parseNumericExpressionWithoutOptionalErrors("2^(3)") assertThat(expression33).hasStructureThatMatches { exponentiation { leftOperand { @@ -996,8 +1099,10 @@ class NumericExpressionParserTest { } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + constant { + withIntegerValueThat().isEqualTo(3) + } } } } @@ -1005,7 +1110,7 @@ class NumericExpressionParserTest { assertThat(expression33).evaluatesToIntegerThat().isEqualTo(8) // Verify that implicit multiple has lower precedence than exponentiation. - val expression34 = parseNumericExpression("2^(3)(4)") + val expression34 = parseNumericExpressionWithoutOptionalErrors("2^(3)(4)") assertThat(expression34).hasStructureThatMatches { multiplication { leftOperand { @@ -1016,15 +1121,19 @@ class NumericExpressionParserTest { } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + constant { + withIntegerValueThat().isEqualTo(3) + } } } } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(4) + group { + constant { + withIntegerValueThat().isEqualTo(4) + } } } } @@ -1034,7 +1143,7 @@ class NumericExpressionParserTest { // An exponentiation can never be an implicit right operand. expectFailureWhenParsingNumericExpression("2^(3)2^2") - val expression35 = parseNumericExpression("2^(3)*2^2") + val expression35 = parseNumericExpressionWithoutOptionalErrors("2^(3)*2^2") assertThat(expression35).hasStructureThatMatches { multiplication { leftOperand { @@ -1045,8 +1154,10 @@ class NumericExpressionParserTest { } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + constant { + withIntegerValueThat().isEqualTo(3) + } } } } @@ -1070,7 +1181,7 @@ class NumericExpressionParserTest { assertThat(expression35).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can be a right operand of an implicit mult if it's grouped. - val expression36 = parseNumericExpression("2^(3)(2^2)") + val expression36 = parseNumericExpressionWithoutOptionalErrors("2^(3)(2^2)") assertThat(expression36).hasStructureThatMatches { multiplication { leftOperand { @@ -1081,22 +1192,26 @@ class NumericExpressionParserTest { } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + constant { + withIntegerValueThat().isEqualTo(3) + } } } } } rightOperand { - exponentiation { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -1108,7 +1223,7 @@ class NumericExpressionParserTest { // An exponentiation can never be an implicit right operand. expectFailureWhenParsingNumericExpression("2^3(4)2^3") - val expression38 = parseNumericExpression("2^3(4)*2^3") + val expression38 = parseNumericExpressionWithoutOptionalErrors("2^3(4)*2^3") assertThat(expression38).hasStructureThatMatches { // 2^3(4)*2^3 multiplication { @@ -1134,8 +1249,10 @@ class NumericExpressionParserTest { } rightOperand { // 4 - constant { - withIntegerValueThat().isEqualTo(4) + group { + constant { + withIntegerValueThat().isEqualTo(4) + } } } } @@ -1169,19 +1286,21 @@ class NumericExpressionParserTest { expectFailureWhenParsingNumericExpression("-2 3") - val expression39 = parseNumericExpression("-(1+2)") + val expression39 = parseNumericExpressionWithoutOptionalErrors("-(1+2)") assertThat(expression39).hasStructureThatMatches { negation { operand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -1193,7 +1312,7 @@ class NumericExpressionParserTest { // Should pass for algebra. expectFailureWhenParsingNumericExpression("-2 x") - val expression40 = parseNumericExpression("-2 (1+2)") + val expression40 = parseNumericExpressionWithoutOptionalErrors("-2 (1+2)") assertThat(expression40).hasStructureThatMatches { // The negation happens last for parity with other common calculators. negation { @@ -1205,15 +1324,17 @@ class NumericExpressionParserTest { } } rightOperand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -1224,7 +1345,7 @@ class NumericExpressionParserTest { } assertThat(expression40).evaluatesToIntegerThat().isEqualTo(-6) - val expression41 = parseNumericExpression("-2^3(4)") + val expression41 = parseNumericExpressionWithoutOptionalErrors("-2^3(4)") assertThat(expression41).hasStructureThatMatches { negation { operand { @@ -1244,8 +1365,10 @@ class NumericExpressionParserTest { } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(4) + group { + constant { + withIntegerValueThat().isEqualTo(4) + } } } } @@ -1254,7 +1377,7 @@ class NumericExpressionParserTest { } assertThat(expression41).evaluatesToIntegerThat().isEqualTo(-32) - val expression43 = parseNumericExpression("√2^2(3)") + val expression43 = parseNumericExpressionWithoutOptionalErrors("√2^2(3)") assertThat(expression43).hasStructureThatMatches { multiplication { leftOperand { @@ -1276,45 +1399,51 @@ class NumericExpressionParserTest { } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + constant { + withIntegerValueThat().isEqualTo(3) + } } } } } assertThat(expression43).evaluatesToIrrationalThat().isWithin(1e-5).of(6.0) - val expression60 = parseNumericExpression("√(2^2(3))") + val expression60 = parseNumericExpressionWithoutOptionalErrors("√(2^2(3))") assertThat(expression60).hasStructureThatMatches { functionCallTo(SQUARE_ROOT) { argument { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } - rightOperand { + } + rightOperand { + group { constant { - withIntegerValueThat().isEqualTo(2) + withIntegerValueThat().isEqualTo(3) } } } } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(3) - } - } } } } } assertThat(expression60).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(12.0)) - val expression42 = parseNumericExpression("-2*-2") + val expression42 = parseNumericExpressionWithoutOptionalErrors("-2*-2") // Note that the following structure is not the same as (-2)*(-2) since unary negation has // higher precedence than multiplication, so it's first & recurses to include the entire // multiplication expression. @@ -1342,7 +1471,7 @@ class NumericExpressionParserTest { } assertThat(expression42).evaluatesToIntegerThat().isEqualTo(4) - val expression44 = parseNumericExpression("2(2)") + val expression44 = parseNumericExpressionWithoutOptionalErrors("2(2)") assertThat(expression44).hasStructureThatMatches { multiplication { leftOperand { @@ -1351,15 +1480,17 @@ class NumericExpressionParserTest { } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } } assertThat(expression44).evaluatesToIntegerThat().isEqualTo(4) - val expression45 = parseNumericExpression("2sqrt(2)") + val expression45 = parseNumericExpressionWithoutOptionalErrors("2sqrt(2)") assertThat(expression45).hasStructureThatMatches { multiplication { leftOperand { @@ -1380,7 +1511,7 @@ class NumericExpressionParserTest { } assertThat(expression45).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) - val expression46 = parseNumericExpression("2√2") + val expression46 = parseNumericExpressionWithoutOptionalErrors("2√2") assertThat(expression46).hasStructureThatMatches { multiplication { leftOperand { @@ -1401,24 +1532,28 @@ class NumericExpressionParserTest { } assertThat(expression46).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) - val expression47 = parseNumericExpression("(2)(2)") + val expression47 = parseNumericExpressionWithoutOptionalErrors("(2)(2)") assertThat(expression47).hasStructureThatMatches { multiplication { leftOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + constant { + withIntegerValueThat().isEqualTo(2) + } } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } } assertThat(expression47).evaluatesToIntegerThat().isEqualTo(4) - val expression48 = parseNumericExpression("sqrt(2)(2)") + val expression48 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)(2)") assertThat(expression48).hasStructureThatMatches { multiplication { leftOperand { @@ -1431,15 +1566,17 @@ class NumericExpressionParserTest { } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } } assertThat(expression48).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) - val expression49 = parseNumericExpression("sqrt(2)sqrt(2)") + val expression49 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)sqrt(2)") assertThat(expression49).hasStructureThatMatches { multiplication { leftOperand { @@ -1464,7 +1601,7 @@ class NumericExpressionParserTest { } assertThat(expression49).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) - val expression50 = parseNumericExpression("√2√2") + val expression50 = parseNumericExpressionWithoutOptionalErrors("√2√2") assertThat(expression50).hasStructureThatMatches { multiplication { leftOperand { @@ -1489,33 +1626,39 @@ class NumericExpressionParserTest { } assertThat(expression50).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) - val expression51 = parseNumericExpression("(2)(2)(2)") + val expression51 = parseNumericExpressionWithoutOptionalErrors("(2)(2)(2)") assertThat(expression51).hasStructureThatMatches { multiplication { leftOperand { multiplication { leftOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + constant { + withIntegerValueThat().isEqualTo(2) + } } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } } assertThat(expression51).evaluatesToIntegerThat().isEqualTo(8) - val expression52 = parseNumericExpression("sqrt(2)sqrt(2)sqrt(2)") + val expression52 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)sqrt(2)sqrt(2)") assertThat(expression52).hasStructureThatMatches { multiplication { leftOperand { @@ -1554,7 +1697,7 @@ class NumericExpressionParserTest { val sqrt2 = sqrt(2.0) assertThat(expression52).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) - val expression53 = parseNumericExpression("√2√2√2") + val expression53 = parseNumericExpressionWithoutOptionalErrors("√2√2√2") assertThat(expression53).hasStructureThatMatches { multiplication { leftOperand { @@ -1598,7 +1741,7 @@ class NumericExpressionParserTest { // Should pass for algebra. expectFailureWhenParsingNumericExpression("2x^2") - val expression54 = parseNumericExpression("2*2/-4+7*2") + val expression54 = parseNumericExpressionWithoutOptionalErrors("2*2/-4+7*2") assertThat(expression54).hasStructureThatMatches { // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) addition { @@ -1656,7 +1799,7 @@ class NumericExpressionParserTest { } assertThat(expression54).evaluatesToIntegerThat().isEqualTo(13) - val expression55 = parseNumericExpression("(3/(1-2))") + val expression55 = parseNumericExpressionWithoutOptionalErrors("3/(1-2)") assertThat(expression55).hasStructureThatMatches { division { leftOperand { @@ -1665,15 +1808,17 @@ class NumericExpressionParserTest { } } rightOperand { - subtraction { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + group { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -1682,24 +1827,28 @@ class NumericExpressionParserTest { } assertThat(expression55).evaluatesToIntegerThat().isEqualTo(-3) - val expression56 = parseNumericExpression("(3)/(1-2)") + val expression56 = parseNumericExpressionWithoutOptionalErrors("(3)/(1-2)") assertThat(expression56).hasStructureThatMatches { division { leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + constant { + withIntegerValueThat().isEqualTo(3) + } } } rightOperand { - subtraction { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + group { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -1708,7 +1857,7 @@ class NumericExpressionParserTest { } assertThat(expression56).evaluatesToIntegerThat().isEqualTo(-3) - val expression57 = parseNumericExpression("3/((1-2))") + val expression57 = parseNumericExpressionWithoutOptionalErrors("3/((1-2))") assertThat(expression57).hasStructureThatMatches { division { leftOperand { @@ -1717,15 +1866,19 @@ class NumericExpressionParserTest { } } rightOperand { - subtraction { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) - } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + group { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } } } } @@ -1744,7 +1897,7 @@ class NumericExpressionParserTest { // TODO: add log string generation for expressions. expectFailureWhenParsingAlgebraicExpression("") - val expression1 = parseAlgebraicExpression("1") + val expression1 = parseAlgebraicExpressionWithoutOptionalErrors("1") assertThat(expression1).hasStructureThatMatches { constant { withIntegerValueThat().isEqualTo(1) @@ -1752,14 +1905,14 @@ class NumericExpressionParserTest { } assertThat(expression1).evaluatesToIntegerThat().isEqualTo(1) - val expression61 = parseAlgebraicExpression("x") + val expression61 = parseAlgebraicExpressionWithoutOptionalErrors("x") assertThat(expression61).hasStructureThatMatches { variable { withNameThat().isEqualTo("x") } } - val expression2 = parseAlgebraicExpression(" 2 ") + val expression2 = parseAlgebraicExpressionWithoutOptionalErrors(" 2 ") assertThat(expression2).hasStructureThatMatches { constant { withIntegerValueThat().isEqualTo(2) @@ -1767,7 +1920,7 @@ class NumericExpressionParserTest { } assertThat(expression2).evaluatesToIntegerThat().isEqualTo(2) - val expression3 = parseAlgebraicExpression(" 2.5 ") + val expression3 = parseAlgebraicExpressionWithoutOptionalErrors(" 2.5 ") assertThat(expression3).hasStructureThatMatches { constant { withIrrationalValueThat().isWithin(1e-5).of(2.5) @@ -1775,14 +1928,14 @@ class NumericExpressionParserTest { } assertThat(expression3).evaluatesToIrrationalThat().isWithin(1e-5).of(2.5) - val expression62 = parseAlgebraicExpression(" y ") + val expression62 = parseAlgebraicExpressionWithoutOptionalErrors(" y ") assertThat(expression62).hasStructureThatMatches { variable { withNameThat().isEqualTo("y") } } - val expression63 = parseAlgebraicExpression(" z x ") + val expression63 = parseAlgebraicExpressionWithoutOptionalErrors(" z x ") assertThat(expression63).hasStructureThatMatches { multiplication { leftOperand { @@ -1798,7 +1951,7 @@ class NumericExpressionParserTest { } } - val expression4 = parseAlgebraicExpression("2^3^2") + val expression4 = parseAlgebraicExpressionWithoutOptionalErrors("2^3^2") assertThat(expression4).hasStructureThatMatches { exponentiation { leftOperand { @@ -1824,19 +1977,21 @@ class NumericExpressionParserTest { } assertThat(expression4).evaluatesToIntegerThat().isEqualTo(512) - val expression23 = parseAlgebraicExpression("(2^3)^2") + val expression23 = parseAlgebraicExpressionWithoutOptionalErrors("(2^3)^2") assertThat(expression23).hasStructureThatMatches { exponentiation { leftOperand { - exponentiation { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(3) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } } } } @@ -1850,7 +2005,7 @@ class NumericExpressionParserTest { } assertThat(expression23).evaluatesToIntegerThat().isEqualTo(64) - val expression24 = parseAlgebraicExpression("512/32/4") + val expression24 = parseAlgebraicExpressionWithoutOptionalErrors("512/32/4") assertThat(expression24).hasStructureThatMatches { division { leftOperand { @@ -1876,7 +2031,7 @@ class NumericExpressionParserTest { } assertThat(expression24).evaluatesToIntegerThat().isEqualTo(4) - val expression25 = parseAlgebraicExpression("512/(32/4)") + val expression25 = parseAlgebraicExpressionWithoutOptionalErrors("512/(32/4)") assertThat(expression25).hasStructureThatMatches { division { leftOperand { @@ -1885,15 +2040,17 @@ class NumericExpressionParserTest { } } rightOperand { - division { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(32) + group { + division { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(32) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(4) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } } } } @@ -1902,7 +2059,7 @@ class NumericExpressionParserTest { } assertThat(expression25).evaluatesToIntegerThat().isEqualTo(64) - val expression5 = parseAlgebraicExpression("sqrt(2)") + val expression5 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)") assertThat(expression5).hasStructureThatMatches { functionCallTo(SQUARE_ROOT) { argument { @@ -1916,7 +2073,7 @@ class NumericExpressionParserTest { expectFailureWhenParsingAlgebraicExpression("sqr(2)") - val expression64 = parseAlgebraicExpression("xyz(2)") + val expression64 = parseAlgebraicExpressionWithoutOptionalErrors("xyz(2)") assertThat(expression64).hasStructureThatMatches { multiplication { leftOperand { @@ -1943,14 +2100,16 @@ class NumericExpressionParserTest { } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } } - val expression6 = parseAlgebraicExpression("732") + val expression6 = parseAlgebraicExpressionWithoutOptionalErrors("732") assertThat(expression6).hasStructureThatMatches { constant { withIntegerValueThat().isEqualTo(732) @@ -1961,7 +2120,7 @@ class NumericExpressionParserTest { expectFailureWhenParsingAlgebraicExpression("73 2") // Verify order of operations between higher & lower precedent operators. - val expression32 = parseAlgebraicExpression("3+4^7") + val expression32 = parseAlgebraicExpressionWithoutOptionalErrors("3+4^7") assertThat(expression32).hasStructureThatMatches { addition { leftOperand { @@ -1987,7 +2146,7 @@ class NumericExpressionParserTest { } assertThat(expression32).evaluatesToIntegerThat().isEqualTo(16387) - val expression7 = parseAlgebraicExpression("3*2-3+4^7*8/3*2+7") + val expression7 = parseAlgebraicExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") assertThat(expression7).hasStructureThatMatches { // To better visualize the precedence & order of operations, see this grouped version: // (((3*2)-3)+((((4^7)*8)/3)*2))+7. @@ -2091,33 +2250,37 @@ class NumericExpressionParserTest { expectFailureWhenParsingAlgebraicExpression("x = √2 × 7 ÷ 4") - val expression8 = parseAlgebraicExpression("(1+2)(3+4)") + val expression8 = parseAlgebraicExpressionWithoutOptionalErrors("(1+2)(3+4)") assertThat(expression8).hasStructureThatMatches { multiplication { leftOperand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } } rightOperand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(4) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } } } } @@ -2129,7 +2292,7 @@ class NumericExpressionParserTest { // Right implicit multiplication of numbers isn't allowed. expectFailureWhenParsingAlgebraicExpression("(1+2)2") - val expression10 = parseAlgebraicExpression("2(1+2)") + val expression10 = parseAlgebraicExpressionWithoutOptionalErrors("2(1+2)") assertThat(expression10).hasStructureThatMatches { multiplication { leftOperand { @@ -2138,15 +2301,17 @@ class NumericExpressionParserTest { } } rightOperand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -2158,7 +2323,7 @@ class NumericExpressionParserTest { // Right implicit multiplication of numbers isn't allowed. expectFailureWhenParsingAlgebraicExpression("sqrt(2)3") - val expression12 = parseAlgebraicExpression("3sqrt(2)") + val expression12 = parseAlgebraicExpressionWithoutOptionalErrors("3sqrt(2)") assertThat(expression12).hasStructureThatMatches { multiplication { leftOperand { @@ -2179,7 +2344,7 @@ class NumericExpressionParserTest { } assertThat(expression12).evaluatesToIrrationalThat().isWithin(1e-5).of(3.0 * sqrt(2.0)) - val expression65 = parseAlgebraicExpression("xsqrt(2)") + val expression65 = parseAlgebraicExpressionWithoutOptionalErrors("xsqrt(2)") assertThat(expression65).hasStructureThatMatches { multiplication { leftOperand { @@ -2199,7 +2364,7 @@ class NumericExpressionParserTest { } } - val expression13 = parseAlgebraicExpression("sqrt(2)*(1+2)*(3-2^5)") + val expression13 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)*(1+2)*(3-2^5)") assertThat(expression13).hasStructureThatMatches { multiplication { leftOperand { @@ -2214,15 +2379,17 @@ class NumericExpressionParserTest { } } rightOperand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -2230,22 +2397,24 @@ class NumericExpressionParserTest { } } rightOperand { - subtraction { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(5) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(5) + } } } } @@ -2256,7 +2425,7 @@ class NumericExpressionParserTest { } assertThat(expression13).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) - val expression58 = parseAlgebraicExpression("sqrt(2)(1+2)(3-2^5)") + val expression58 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)(1+2)(3-2^5)") assertThat(expression58).hasStructureThatMatches { multiplication { leftOperand { @@ -2271,15 +2440,17 @@ class NumericExpressionParserTest { } } rightOperand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -2287,22 +2458,24 @@ class NumericExpressionParserTest { } } rightOperand { - subtraction { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(5) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(5) + } } } } @@ -2313,15 +2486,19 @@ class NumericExpressionParserTest { } assertThat(expression58).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) - val expression14 = parseAlgebraicExpression("((3))") + val expression14 = parseAlgebraicExpressionWithoutOptionalErrors("((3))") assertThat(expression14).hasStructureThatMatches { - constant { - withIntegerValueThat().isEqualTo(3) + group { + group { + constant { + withIntegerValueThat().isEqualTo(3) + } + } } } assertThat(expression14).evaluatesToIntegerThat().isEqualTo(3) - val expression15 = parseAlgebraicExpression("++3") + val expression15 = parseAlgebraicExpressionWithoutOptionalErrors("++3") assertThat(expression15).hasStructureThatMatches { positive { operand { @@ -2337,7 +2514,7 @@ class NumericExpressionParserTest { } assertThat(expression15).evaluatesToIntegerThat().isEqualTo(3) - val expression16 = parseAlgebraicExpression("--4") + val expression16 = parseAlgebraicExpressionWithoutOptionalErrors("--4") assertThat(expression16).hasStructureThatMatches { negation { operand { @@ -2353,7 +2530,7 @@ class NumericExpressionParserTest { } assertThat(expression16).evaluatesToIntegerThat().isEqualTo(4) - val expression17 = parseAlgebraicExpression("1+-4") + val expression17 = parseAlgebraicExpressionWithoutOptionalErrors("1+-4") assertThat(expression17).hasStructureThatMatches { addition { leftOperand { @@ -2374,7 +2551,7 @@ class NumericExpressionParserTest { } assertThat(expression17).evaluatesToIntegerThat().isEqualTo(-3) - val expression18 = parseAlgebraicExpression("1++4") + val expression18 = parseAlgebraicExpressionWithoutOptionalErrors("1++4") assertThat(expression18).hasStructureThatMatches { addition { leftOperand { @@ -2395,7 +2572,7 @@ class NumericExpressionParserTest { } assertThat(expression18).evaluatesToIntegerThat().isEqualTo(5) - val expression19 = parseAlgebraicExpression("1--4") + val expression19 = parseAlgebraicExpressionWithoutOptionalErrors("1--4") assertThat(expression19).hasStructureThatMatches { subtraction { leftOperand { @@ -2418,7 +2595,7 @@ class NumericExpressionParserTest { expectFailureWhenParsingAlgebraicExpression("1-^-4") - val expression20 = parseAlgebraicExpression("√2 × 7 ÷ 4") + val expression20 = parseAlgebraicExpressionWithoutOptionalErrors("√2 × 7 ÷ 4") assertThat(expression20).hasStructureThatMatches { division { leftOperand { @@ -2450,7 +2627,7 @@ class NumericExpressionParserTest { expectFailureWhenParsingAlgebraicExpression("1+2 &asdf") - val expression21 = parseAlgebraicExpression("sqrt(2)sqrt(3)sqrt(4)") + val expression21 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)sqrt(3)sqrt(4)") // Note that this tree demonstrates left associativity. assertThat(expression21).hasStructureThatMatches { multiplication { @@ -2492,44 +2669,48 @@ class NumericExpressionParserTest { .isWithin(1e-5) .of(sqrt(2.0) * sqrt(3.0) * sqrt(4.0)) - val expression22 = parseAlgebraicExpression("(1+2)(3-7^2)(5+-17)") + val expression22 = parseAlgebraicExpressionWithoutOptionalErrors("(1+2)(3-7^2)(5+-17)") assertThat(expression22).hasStructureThatMatches { multiplication { leftOperand { multiplication { leftOperand { // 1+2 - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } } rightOperand { // 3-7^2 - subtraction { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(7) + rightOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(7) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -2540,17 +2721,19 @@ class NumericExpressionParserTest { } rightOperand { // 5+-17 - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(5) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(5) + } } - } - rightOperand { - negation { - operand { - constant { - withIntegerValueThat().isEqualTo(17) + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(17) + } } } } @@ -2561,7 +2744,7 @@ class NumericExpressionParserTest { } assertThat(expression22).evaluatesToIntegerThat().isEqualTo(1656) - val expression26 = parseAlgebraicExpression("3^-2") + val expression26 = parseAlgebraicExpressionWithoutOptionalErrors("3^-2") assertThat(expression26).hasStructureThatMatches { exponentiation { leftOperand { @@ -2587,21 +2770,23 @@ class NumericExpressionParserTest { hasDenominatorThat().isEqualTo(9) } - val expression27 = parseAlgebraicExpression("(3^-2)^(3^-2)") + val expression27 = parseAlgebraicExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") assertThat(expression27).hasStructureThatMatches { exponentiation { leftOperand { - exponentiation { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } } - } - rightOperand { - negation { - operand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -2609,17 +2794,19 @@ class NumericExpressionParserTest { } } rightOperand { - exponentiation { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } } - } - rightOperand { - negation { - operand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + negation { + operand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -2630,7 +2817,7 @@ class NumericExpressionParserTest { } assertThat(expression27).evaluatesToIrrationalThat().isWithin(1e-5).of(0.78338103693) - val expression28 = parseAlgebraicExpression("1-3^sqrt(4)") + val expression28 = parseAlgebraicExpressionWithoutOptionalErrors("1-3^sqrt(4)") assertThat(expression28).hasStructureThatMatches { subtraction { leftOperand { @@ -2662,7 +2849,7 @@ class NumericExpressionParserTest { // "Hard" order of operation problems loosely based on & other problems that can often stump // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. - val expression29 = parseAlgebraicExpression("3÷2*(3+4)") + val expression29 = parseAlgebraicExpressionWithoutOptionalErrors("3÷2*(3+4)") assertThat(expression29).hasStructureThatMatches { multiplication { leftOperand { @@ -2680,15 +2867,17 @@ class NumericExpressionParserTest { } } rightOperand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(4) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } } } } @@ -2697,7 +2886,7 @@ class NumericExpressionParserTest { } assertThat(expression29).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) - val expression59 = parseAlgebraicExpression("3÷2(3+4)") + val expression59 = parseAlgebraicExpressionWithoutOptionalErrors("3÷2(3+4)") assertThat(expression59).hasStructureThatMatches { multiplication { leftOperand { @@ -2715,15 +2904,17 @@ class NumericExpressionParserTest { } } rightOperand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(3) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(4) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(4) + } } } } @@ -2739,33 +2930,39 @@ class NumericExpressionParserTest { expectFailureWhenParsingAlgebraicExpression("2^2 2") - val expression31 = parseAlgebraicExpression("(3)(4)(5)") + val expression31 = parseAlgebraicExpressionWithoutOptionalErrors("(3)(4)(5)") assertThat(expression31).hasStructureThatMatches { multiplication { leftOperand { multiplication { leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + constant { + withIntegerValueThat().isEqualTo(3) + } } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(4) + group { + constant { + withIntegerValueThat().isEqualTo(4) + } } } } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(5) + group { + constant { + withIntegerValueThat().isEqualTo(5) + } } } } } assertThat(expression31).evaluatesToIntegerThat().isEqualTo(60) - val expression33 = parseAlgebraicExpression("2^(3)") + val expression33 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)") assertThat(expression33).hasStructureThatMatches { exponentiation { leftOperand { @@ -2774,8 +2971,10 @@ class NumericExpressionParserTest { } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + constant { + withIntegerValueThat().isEqualTo(3) + } } } } @@ -2783,7 +2982,7 @@ class NumericExpressionParserTest { assertThat(expression33).evaluatesToIntegerThat().isEqualTo(8) // Verify that implicit multiple has lower precedence than exponentiation. - val expression34 = parseAlgebraicExpression("2^(3)(4)") + val expression34 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(4)") assertThat(expression34).hasStructureThatMatches { multiplication { leftOperand { @@ -2794,15 +2993,19 @@ class NumericExpressionParserTest { } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + constant { + withIntegerValueThat().isEqualTo(3) + } } } } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(4) + group { + constant { + withIntegerValueThat().isEqualTo(4) + } } } } @@ -2812,7 +3015,7 @@ class NumericExpressionParserTest { // An exponentiation can never be an implicit right operand. expectFailureWhenParsingAlgebraicExpression("2^(3)2^2") - val expression35 = parseAlgebraicExpression("2^(3)*2^2") + val expression35 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)*2^2") assertThat(expression35).hasStructureThatMatches { multiplication { leftOperand { @@ -2823,8 +3026,10 @@ class NumericExpressionParserTest { } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + constant { + withIntegerValueThat().isEqualTo(3) + } } } } @@ -2848,7 +3053,7 @@ class NumericExpressionParserTest { assertThat(expression35).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can be a right operand of an implicit mult if it's grouped. - val expression36 = parseAlgebraicExpression("2^(3)(2^2)") + val expression36 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(2^2)") assertThat(expression36).hasStructureThatMatches { multiplication { leftOperand { @@ -2859,22 +3064,26 @@ class NumericExpressionParserTest { } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + constant { + withIntegerValueThat().isEqualTo(3) + } } } } } rightOperand { - exponentiation { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -2886,7 +3095,7 @@ class NumericExpressionParserTest { // An exponentiation can never be an implicit right operand. expectFailureWhenParsingAlgebraicExpression("2^3(4)2^3") - val expression38 = parseAlgebraicExpression("2^3(4)*2^3") + val expression38 = parseAlgebraicExpressionWithoutOptionalErrors("2^3(4)*2^3") assertThat(expression38).hasStructureThatMatches { // 2^3(4)*2^3 multiplication { @@ -2912,8 +3121,10 @@ class NumericExpressionParserTest { } rightOperand { // 4 - constant { - withIntegerValueThat().isEqualTo(4) + group { + constant { + withIntegerValueThat().isEqualTo(4) + } } } } @@ -2947,19 +3158,21 @@ class NumericExpressionParserTest { expectFailureWhenParsingAlgebraicExpression("-2 3") - val expression39 = parseAlgebraicExpression("-(1+2)") + val expression39 = parseAlgebraicExpressionWithoutOptionalErrors("-(1+2)") assertThat(expression39).hasStructureThatMatches { negation { operand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -2969,7 +3182,7 @@ class NumericExpressionParserTest { assertThat(expression39).evaluatesToIntegerThat().isEqualTo(-3) // Should pass for algebra. - val expression66 = parseAlgebraicExpression("-2 x") + val expression66 = parseAlgebraicExpressionWithoutOptionalErrors("-2 x") assertThat(expression66).hasStructureThatMatches { negation { operand { @@ -2989,7 +3202,7 @@ class NumericExpressionParserTest { } } - val expression40 = parseAlgebraicExpression("-2 (1+2)") + val expression40 = parseAlgebraicExpressionWithoutOptionalErrors("-2 (1+2)") assertThat(expression40).hasStructureThatMatches { // The negation happens last for parity with other common calculators. negation { @@ -3001,15 +3214,17 @@ class NumericExpressionParserTest { } } rightOperand { - addition { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + group { + addition { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -3020,7 +3235,7 @@ class NumericExpressionParserTest { } assertThat(expression40).evaluatesToIntegerThat().isEqualTo(-6) - val expression41 = parseAlgebraicExpression("-2^3(4)") + val expression41 = parseAlgebraicExpressionWithoutOptionalErrors("-2^3(4)") assertThat(expression41).hasStructureThatMatches { negation { operand { @@ -3040,8 +3255,10 @@ class NumericExpressionParserTest { } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(4) + group { + constant { + withIntegerValueThat().isEqualTo(4) + } } } } @@ -3050,7 +3267,7 @@ class NumericExpressionParserTest { } assertThat(expression41).evaluatesToIntegerThat().isEqualTo(-32) - val expression43 = parseAlgebraicExpression("√2^2(3)") + val expression43 = parseAlgebraicExpressionWithoutOptionalErrors("√2^2(3)") assertThat(expression43).hasStructureThatMatches { multiplication { leftOperand { @@ -3072,45 +3289,51 @@ class NumericExpressionParserTest { } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + constant { + withIntegerValueThat().isEqualTo(3) + } } } } } assertThat(expression43).evaluatesToIrrationalThat().isWithin(1e-5).of(6.0) - val expression60 = parseAlgebraicExpression("√(2^2(3))") + val expression60 = parseAlgebraicExpressionWithoutOptionalErrors("√(2^2(3))") assertThat(expression60).hasStructureThatMatches { functionCallTo(SQUARE_ROOT) { argument { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } - rightOperand { + } + rightOperand { + group { constant { - withIntegerValueThat().isEqualTo(2) + withIntegerValueThat().isEqualTo(3) } } } } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(3) - } - } } } } } assertThat(expression60).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(12.0)) - val expression42 = parseAlgebraicExpression("-2*-2") + val expression42 = parseAlgebraicExpressionWithoutOptionalErrors("-2*-2") // Note that the following structure is not the same as (-2)*(-2) since unary negation has // higher precedence than multiplication, so it's first & recurses to include the entire // multiplication expression. @@ -3138,7 +3361,7 @@ class NumericExpressionParserTest { } assertThat(expression42).evaluatesToIntegerThat().isEqualTo(4) - val expression44 = parseAlgebraicExpression("2(2)") + val expression44 = parseAlgebraicExpressionWithoutOptionalErrors("2(2)") assertThat(expression44).hasStructureThatMatches { multiplication { leftOperand { @@ -3147,15 +3370,17 @@ class NumericExpressionParserTest { } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } } assertThat(expression44).evaluatesToIntegerThat().isEqualTo(4) - val expression45 = parseAlgebraicExpression("2sqrt(2)") + val expression45 = parseAlgebraicExpressionWithoutOptionalErrors("2sqrt(2)") assertThat(expression45).hasStructureThatMatches { multiplication { leftOperand { @@ -3176,7 +3401,7 @@ class NumericExpressionParserTest { } assertThat(expression45).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) - val expression46 = parseAlgebraicExpression("2√2") + val expression46 = parseAlgebraicExpressionWithoutOptionalErrors("2√2") assertThat(expression46).hasStructureThatMatches { multiplication { leftOperand { @@ -3197,24 +3422,28 @@ class NumericExpressionParserTest { } assertThat(expression46).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) - val expression47 = parseAlgebraicExpression("(2)(2)") + val expression47 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)") assertThat(expression47).hasStructureThatMatches { multiplication { leftOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + constant { + withIntegerValueThat().isEqualTo(2) + } } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } } assertThat(expression47).evaluatesToIntegerThat().isEqualTo(4) - val expression48 = parseAlgebraicExpression("sqrt(2)(2)") + val expression48 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)(2)") assertThat(expression48).hasStructureThatMatches { multiplication { leftOperand { @@ -3227,15 +3456,17 @@ class NumericExpressionParserTest { } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } } assertThat(expression48).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) - val expression49 = parseAlgebraicExpression("sqrt(2)sqrt(2)") + val expression49 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)sqrt(2)") assertThat(expression49).hasStructureThatMatches { multiplication { leftOperand { @@ -3260,7 +3491,7 @@ class NumericExpressionParserTest { } assertThat(expression49).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) - val expression50 = parseAlgebraicExpression("√2√2") + val expression50 = parseAlgebraicExpressionWithoutOptionalErrors("√2√2") assertThat(expression50).hasStructureThatMatches { multiplication { leftOperand { @@ -3285,33 +3516,39 @@ class NumericExpressionParserTest { } assertThat(expression50).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) - val expression51 = parseAlgebraicExpression("(2)(2)(2)") + val expression51 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)(2)") assertThat(expression51).hasStructureThatMatches { multiplication { leftOperand { multiplication { leftOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + constant { + withIntegerValueThat().isEqualTo(2) + } } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } } rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } } assertThat(expression51).evaluatesToIntegerThat().isEqualTo(8) - val expression52 = parseAlgebraicExpression("sqrt(2)sqrt(2)sqrt(2)") + val expression52 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)sqrt(2)sqrt(2)") assertThat(expression52).hasStructureThatMatches { multiplication { leftOperand { @@ -3350,7 +3587,7 @@ class NumericExpressionParserTest { val sqrt2 = sqrt(2.0) assertThat(expression52).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) - val expression53 = parseAlgebraicExpression("√2√2√2") + val expression53 = parseAlgebraicExpressionWithoutOptionalErrors("√2√2√2") assertThat(expression53).hasStructureThatMatches { multiplication { leftOperand { @@ -3392,7 +3629,7 @@ class NumericExpressionParserTest { expectFailureWhenParsingAlgebraicExpression("x7") // Should pass for algebra. - val expression67 = parseAlgebraicExpression("2x^2y^-3") + val expression67 = parseAlgebraicExpressionWithoutOptionalErrors("2x^2y^-3") assertThat(expression67).hasStructureThatMatches { // 2x^2y^-3 -> (2*(x^2))*(y^(-3)) multiplication { @@ -3449,7 +3686,7 @@ class NumericExpressionParserTest { } } - val expression54 = parseAlgebraicExpression("2*2/-4+7*2") + val expression54 = parseAlgebraicExpressionWithoutOptionalErrors("2*2/-4+7*2") assertThat(expression54).hasStructureThatMatches { // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) addition { @@ -3507,7 +3744,7 @@ class NumericExpressionParserTest { } assertThat(expression54).evaluatesToIntegerThat().isEqualTo(13) - val expression55 = parseAlgebraicExpression("(3/(1-2))") + val expression55 = parseAlgebraicExpressionWithoutOptionalErrors("3/(1-2)") assertThat(expression55).hasStructureThatMatches { division { leftOperand { @@ -3516,15 +3753,17 @@ class NumericExpressionParserTest { } } rightOperand { - subtraction { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + group { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -3533,24 +3772,28 @@ class NumericExpressionParserTest { } assertThat(expression55).evaluatesToIntegerThat().isEqualTo(-3) - val expression56 = parseAlgebraicExpression("(3)/(1-2)") + val expression56 = parseAlgebraicExpressionWithoutOptionalErrors("(3)/(1-2)") assertThat(expression56).hasStructureThatMatches { division { leftOperand { - constant { - withIntegerValueThat().isEqualTo(3) + group { + constant { + withIntegerValueThat().isEqualTo(3) + } } } rightOperand { - subtraction { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) + group { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } } } } @@ -3559,7 +3802,7 @@ class NumericExpressionParserTest { } assertThat(expression56).evaluatesToIntegerThat().isEqualTo(-3) - val expression57 = parseAlgebraicExpression("3/((1-2))") + val expression57 = parseAlgebraicExpressionWithoutOptionalErrors("3/((1-2))") assertThat(expression57).hasStructureThatMatches { division { leftOperand { @@ -3568,15 +3811,19 @@ class NumericExpressionParserTest { } } rightOperand { - subtraction { - leftOperand { - constant { - withIntegerValueThat().isEqualTo(1) - } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(2) + group { + group { + subtraction { + leftOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } + } + rightOperand { + constant { + withIntegerValueThat().isEqualTo(2) + } + } } } } @@ -3594,7 +3841,7 @@ class NumericExpressionParserTest { expectFailureWhenParsingAlgebraicEquation(" x =") expectFailureWhenParsingAlgebraicEquation(" = y") - val equation1 = parseAlgebraicEquation("x = 1") + val equation1 = parseAlgebraicEquationWithoutOptionalErrors("x = 1") assertThat(equation1).hasLeftHandSideThat().hasStructureThatMatches { variable { withNameThat().isEqualTo("x") @@ -3603,7 +3850,7 @@ class NumericExpressionParserTest { assertThat(equation1).hasRightHandSideThat().evaluatesToIntegerThat().isEqualTo(1) val equation2 = - parseAlgebraicEquation("y = mx + b", allowedVariables = listOf("x", "y", "b", "m")) + parseAlgebraicEquationWithoutOptionalErrors("y = mx + b", allowedVariables = listOf("x", "y", "b", "m")) assertThat(equation2).hasLeftHandSideThat().hasStructureThatMatches { variable { withNameThat().isEqualTo("y") @@ -3633,7 +3880,7 @@ class NumericExpressionParserTest { } } - val equation3 = parseAlgebraicEquation("y = (x+1)^2") + val equation3 = parseAlgebraicEquationWithoutOptionalErrors("y = (x+1)^2") assertThat(equation3).hasLeftHandSideThat().hasStructureThatMatches { variable { withNameThat().isEqualTo("y") @@ -3642,15 +3889,17 @@ class NumericExpressionParserTest { assertThat(equation3).hasRightHandSideThat().hasStructureThatMatches { exponentiation { leftOperand { - addition { - leftOperand { - variable { - withNameThat().isEqualTo("x") + group { + addition { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(1) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } } } @@ -3663,7 +3912,7 @@ class NumericExpressionParserTest { } } - val equation4 = parseAlgebraicEquation("y = (x+1)(x-1)") + val equation4 = parseAlgebraicEquationWithoutOptionalErrors("y = (x+1)(x-1)") assertThat(equation4).hasLeftHandSideThat().hasStructureThatMatches { variable { withNameThat().isEqualTo("y") @@ -3672,29 +3921,33 @@ class NumericExpressionParserTest { assertThat(equation4).hasRightHandSideThat().hasStructureThatMatches { multiplication { leftOperand { - addition { - leftOperand { - variable { - withNameThat().isEqualTo("x") + group { + addition { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(1) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } } } } rightOperand { - subtraction { - leftOperand { - variable { - withNameThat().isEqualTo("x") + group { + subtraction { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } } - } - rightOperand { - constant { - withIntegerValueThat().isEqualTo(1) + rightOperand { + constant { + withIntegerValueThat().isEqualTo(1) + } } } } @@ -3706,7 +3959,7 @@ class NumericExpressionParserTest { expectFailureWhenParsingAlgebraicEquation("y 2 = (x+1)(x-1)") val equation5 = - parseAlgebraicEquation("a*x^2 + b*x + c = 0", allowedVariables = listOf("x", "a", "b", "c")) + parseAlgebraicEquationWithoutOptionalErrors("a*x^2 + b*x + c = 0", allowedVariables = listOf("x", "a", "b", "c")) assertThat(equation5).hasLeftHandSideThat().hasStructureThatMatches { addition { leftOperand { @@ -3795,6 +4048,7 @@ class NumericExpressionParserTest { return checkNotNull(real) // Just to remove the nullable operator; the actual check is above. } + // TODO: update DSL to not have return values (since it's unnecessary). @ExpressionComparatorMarker class ExpressionComparator private constructor(private val expression: MathExpression) { // TODO: convert to constant comparator? @@ -3863,6 +4117,10 @@ class NumericExpressionParserTest { ).also(init) } + fun group(init: ExpressionComparator.() -> Unit): ExpressionComparator { + return createFromExpression(expression.group).also(init) + } + internal companion object { fun createFromExpression(expression: MathExpression): ExpressionComparator = ExpressionComparator(expression) @@ -3996,57 +4254,68 @@ class NumericExpressionParserTest { private companion object { // TODO: fix helper API. - private fun expectFailureWhenParsingNumericExpression(expression: String) { - assertThat(parseNumericExpressionInternal(expression)) - .isInstanceOf(MathParsingResult.Failure::class.java) + private fun expectFailureWhenParsingNumericExpression(expression: String): MathParsingError { + val result = parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseNumericExpressionWithoutOptionalErrors(expression: String): MathExpression { + return (parseNumericExpressionInternal(expression, ErrorCheckingMode.REQUIRED_ONLY) as MathParsingResult.Success).result } - private fun parseNumericExpression(expression: String): MathExpression { - return (parseNumericExpressionInternal(expression) as MathParsingResult.Success).result + private fun parseNumericExpressionWithAllErrors(expression: String): MathExpression { + return (parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) as MathParsingResult.Success).result } private fun parseNumericExpressionInternal( - expression: String + expression: String, errorCheckingMode: ErrorCheckingMode ): MathParsingResult { - return NumericExpressionParser.parseNumericExpression(expression) + return NumericExpressionParser.parseNumericExpression(expression, errorCheckingMode) } private fun expectFailureWhenParsingAlgebraicExpression(expression: String) { - assertThat(parseAlgebraicExpressionInternal(expression)) + assertThat(parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS)) .isInstanceOf(MathParsingResult.Failure::class.java) } - private fun parseAlgebraicExpression( + private fun parseAlgebraicExpressionWithoutOptionalErrors( expression: String, allowedVariables: List = listOf("x", "y", "z") ): MathExpression { - return (parseAlgebraicExpressionInternal(expression, allowedVariables) as MathParsingResult.Success).result + return (parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables) as MathParsingResult.Success).result } private fun parseAlgebraicExpressionInternal( expression: String, + errorCheckingMode: ErrorCheckingMode, allowedVariables: List = listOf("x", "y", "z") ): MathParsingResult { - return NumericExpressionParser.parseAlgebraicExpression(expression, allowedVariables) + return NumericExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) } private fun expectFailureWhenParsingAlgebraicEquation(expression: String) { - assertThat(parseAlgebraicEquationInternal(expression)) + assertThat(parseAlgebraicEquationInternal(expression, ErrorCheckingMode.ALL_ERRORS)) .isInstanceOf(MathParsingResult.Failure::class.java) } - private fun parseAlgebraicEquation( + private fun parseAlgebraicEquationWithoutOptionalErrors( expression: String, allowedVariables: List = listOf("x", "y", "z") ): MathEquation { - return (parseAlgebraicEquationInternal(expression, allowedVariables) as MathParsingResult.Success).result + return (parseAlgebraicEquationInternal(expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables) as MathParsingResult.Success).result } private fun parseAlgebraicEquationInternal( expression: String, + errorCheckingMode: ErrorCheckingMode, allowedVariables: List = listOf("x", "y", "z") ): MathParsingResult { - return NumericExpressionParser.parseAlgebraicEquation(expression, allowedVariables) + return NumericExpressionParser.parseAlgebraicEquation( + expression, allowedVariables, errorCheckingMode + ) } private fun assertThat(actual: MathExpression): MathExpressionSubject = From dd97b84fe9f6e4870cd8a03b9aaf634f111ae456 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 18 Nov 2021 13:20:15 -0800 Subject: [PATCH 080/289] More errors added. + some infrastructural work: the parser is switched to force exhaustive when statements to ensure all tokens are considered in all important places. While this is harder to maintain and less readable/flexible, it's more correct. I think per error handling practices that correctness is actually more important than maintenance here. --- .../android/util/math/MathParsingError.kt | 2 +- .../util/math/NumericExpressionParser.kt | 363 ++++++++---------- .../util/math/NumericExpressionParserTest.kt | 57 ++- 3 files changed, 221 insertions(+), 201 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt index dd2c4fc243d..e7af82d3c47 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt @@ -22,7 +22,7 @@ sealed class MathParsingError { val rawExpression: String, val expression: MathExpression ): MathParsingError() - data class UnnecessarySymbolsError(val invalidCharacter: Char): MathParsingError() + data class UnnecessarySymbolsError(val invalidSymbol: String): MathParsingError() data class NumberAfterVariableError(val number: Real, val variable: String): MathParsingError() diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt index bd534d63045..60f06b0b4c3 100644 --- a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt @@ -18,6 +18,7 @@ import org.oppia.android.util.math.MathTokenizer2.Companion.Token.DivideSymbol import org.oppia.android.util.math.MathTokenizer2.Companion.Token.EqualsSymbol import org.oppia.android.util.math.MathTokenizer2.Companion.Token.ExponentiationSymbol import org.oppia.android.util.math.MathTokenizer2.Companion.Token.FunctionName +import org.oppia.android.util.math.MathTokenizer2.Companion.Token.InvalidToken import org.oppia.android.util.math.MathTokenizer2.Companion.Token.LeftParenthesisSymbol import org.oppia.android.util.math.MathTokenizer2.Companion.Token.MinusSymbol import org.oppia.android.util.math.MathTokenizer2.Companion.Token.MultiplySymbol @@ -68,19 +69,13 @@ class NumericExpressionParser private constructor( val includeOptionalErrors = parseContext.errorCheckingMode.includesOptionalErrors() return when { includeOptionalErrors && firstMultiRedundantGroup != null -> { - val subExpression = - parseContext.rawExpression.substring( - firstMultiRedundantGroup.parseStartIndex, firstMultiRedundantGroup.parseEndIndex - ) + val subExpression = parseContext.extractSubexpression(firstMultiRedundantGroup) MathParsingError.MultipleRedundantParenthesesError(subExpression, firstMultiRedundantGroup) } includeOptionalErrors && expression.expressionTypeCase == GROUP -> MathParsingError.SingleRedundantParenthesesError(parseContext.rawExpression, expression) includeOptionalErrors && nextRedundantGroup != null -> { - val subExpression = - parseContext.rawExpression.substring( - nextRedundantGroup.parseStartIndex, nextRedundantGroup.parseEndIndex - ) + val subExpression = parseContext.extractSubexpression(nextRedundantGroup) MathParsingError.RedundantParenthesesForIndividualTermsError( subExpression, nextRedundantGroup ) @@ -89,43 +84,17 @@ class NumericExpressionParser private constructor( } } - private fun MathExpression.findFirstMultiRedundantGroup(): MathExpression? { - return when (expressionTypeCase) { - BINARY_OPERATION -> { - binaryOperation.leftOperand.findFirstMultiRedundantGroup() - ?: binaryOperation.rightOperand.findFirstMultiRedundantGroup() - } - UNARY_OPERATION -> unaryOperation.operand.findFirstMultiRedundantGroup() - FUNCTION_CALL -> functionCall.argument.findFirstMultiRedundantGroup() - GROUP -> group.takeIf { it.expressionTypeCase == GROUP } ?: group.findFirstMultiRedundantGroup() - CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null - } - } - - private fun MathExpression.findNextRedundantGroup(): MathExpression? { - return when (expressionTypeCase) { - BINARY_OPERATION -> { - binaryOperation.leftOperand.findNextRedundantGroup() - ?: binaryOperation.rightOperand.findNextRedundantGroup() - } - UNARY_OPERATION -> unaryOperation.operand.findNextRedundantGroup() - FUNCTION_CALL -> functionCall.argument.findNextRedundantGroup() - GROUP -> { - group.takeIf { it.expressionTypeCase in listOf(CONSTANT, VARIABLE) } - ?: group.findNextRedundantGroup() - } - CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null - } - } - private fun ensureNoRemainingTokens(): MathParsingError? { // Make sure all tokens were consumed (otherwise there are trailing tokens which invalidate the // whole grammar). return if (tokens.hasNext()) { - when (tokens.peek()) { + when (val nextToken = tokens.peek()) { is LeftParenthesisSymbol, is RightParenthesisSymbol -> MathParsingError.UnbalancedParenthesesError - else -> MathParsingError.GenericError + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is FunctionName, is MinusSymbol, is MultiplySymbol, is PlusSymbol, + is SquareRootSymbol, is VariableName, null -> MathParsingError.GenericError + is InvalidToken -> nextToken.toError() } } else null } @@ -133,7 +102,7 @@ class NumericExpressionParser private constructor( private fun parseGenericEquation(): MathParsingResult { // algebraic_equation = generic_expression , equals_operator , generic_expression ; val lhsResult = parseGenericExpression().also { - consumeTokenOfType { MathParsingError.GenericError } + consumeTokenOfType() } val rhsResult = lhsResult.flatMap { parseGenericExpression() } return lhsResult.combineWith(rhsResult) { lhs, rhs -> @@ -154,14 +123,17 @@ class NumericExpressionParser private constructor( // generic_add_sub_expression = // generic_mult_div_expression , { generic_add_sub_expression_rhs } ; var lastLhsResult = parseGenericMultDivExpression() - while (!lastLhsResult.isFailure() && hasNextGenericAddSubExpressionRhs()) { + while (!lastLhsResult.isFailure()) { // generic_add_sub_expression_rhs = generic_add_expression_rhs | generic_sub_expression_rhs ; - val (operator, rhsResult) = when { - hasNextGenericAddExpressionRhs() -> + val (operator, rhsResult) = when (tokens.peek()) { + is PlusSymbol -> MathBinaryOperation.Operator.ADD to parseGenericAddExpressionRhs() - hasNextGenericSubExpressionRhs() -> + is MinusSymbol -> MathBinaryOperation.Operator.SUBTRACT to parseGenericSubExpressionRhs() - else -> return MathParsingError.GenericError.toFailure() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is FunctionName, is InvalidToken, is LeftParenthesisSymbol, + is MultiplySymbol, is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, null -> + break // Not a match to the expression. } lastLhsResult = lastLhsResult.combineWith(rhsResult) { lhs, rhs -> @@ -180,27 +152,16 @@ class NumericExpressionParser private constructor( return lastLhsResult } - private fun hasNextGenericAddSubExpressionRhs() = - hasNextGenericAddExpressionRhs() || hasNextGenericSubExpressionRhs() - - private fun hasNextGenericAddExpressionRhs(): Boolean = tokens.peek() is PlusSymbol - private fun parseGenericAddExpressionRhs(): MathParsingResult { // generic_add_expression_rhs = plus_operator , generic_mult_div_expression ; - return consumeTokenOfType { - MathParsingError.GenericError - }.flatMap { + return consumeTokenOfType().flatMap { parseGenericMultDivExpression() } } - private fun hasNextGenericSubExpressionRhs(): Boolean = tokens.peek() is MinusSymbol - private fun parseGenericSubExpressionRhs(): MathParsingResult { // generic_sub_expression_rhs = minus_operator , generic_mult_div_expression ; - return consumeTokenOfType { - MathParsingError.GenericError - }.flatMap { + return consumeTokenOfType().flatMap { parseGenericMultDivExpression() } } @@ -209,19 +170,25 @@ class NumericExpressionParser private constructor( // generic_mult_div_expression = // generic_exp_expression , { generic_mult_div_expression_rhs } ; var lastLhsResult = parseGenericExpExpression() - while (!lastLhsResult.isFailure() && hasNextGenericMultDivExpressionRhs()) { + while (!lastLhsResult.isFailure()) { // generic_mult_div_expression_rhs = // generic_mult_expression_rhs // | generic_div_expression_rhs // | generic_implicit_mult_expression_rhs ; - val (operator, rhsResult) = when { - hasNextGenericMultExpressionRhs() -> + val (operator, rhsResult) = when (tokens.peek()) { + is MultiplySymbol -> MathBinaryOperation.Operator.MULTIPLY to parseGenericMultExpressionRhs() - hasNextGenericDivExpressionRhs() -> - MathBinaryOperation.Operator.DIVIDE to parseGenericDivExpressionRhs() - hasNextGenericImplicitMultExpressionRhs() -> + is DivideSymbol -> MathBinaryOperation.Operator.DIVIDE to parseGenericDivExpressionRhs() + is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> MathBinaryOperation.Operator.MULTIPLY to parseGenericImplicitMultExpressionRhs() - else -> return MathParsingError.GenericError.toFailure() + is VariableName -> { + if (parseContext is AlgebraicExpressionContext) { + MathBinaryOperation.Operator.MULTIPLY to parseGenericImplicitMultExpressionRhs() + } else break + } + // Not a match to the expression. + is PositiveInteger, is PositiveRealNumber, is EqualsSymbol, is ExponentiationSymbol, + is InvalidToken, is MinusSymbol, is PlusSymbol, is RightParenthesisSymbol, null -> break } // Compute the next LHS if there is further multiplication/division. @@ -240,40 +207,20 @@ class NumericExpressionParser private constructor( return lastLhsResult } - private fun hasNextGenericMultDivExpressionRhs(): Boolean = - hasNextGenericMultExpressionRhs() - || hasNextGenericDivExpressionRhs() - || hasNextGenericImplicitMultExpressionRhs() - - private fun hasNextGenericMultExpressionRhs(): Boolean = tokens.peek() is MultiplySymbol - private fun parseGenericMultExpressionRhs(): MathParsingResult { // generic_mult_expression_rhs = multiplication_operator , generic_exp_expression ; - return consumeTokenOfType { - MathParsingError.GenericError - }.flatMap { + return consumeTokenOfType().flatMap { parseGenericExpExpression() } } - private fun hasNextGenericDivExpressionRhs(): Boolean = tokens.peek() is DivideSymbol - private fun parseGenericDivExpressionRhs(): MathParsingResult { // generic_div_expression_rhs = division_operator , generic_exp_expression ; - return consumeTokenOfType { - MathParsingError.GenericError - }.flatMap { + return consumeTokenOfType().flatMap { parseGenericExpExpression() } } - private fun hasNextGenericImplicitMultExpressionRhs(): Boolean { - return when (parseContext) { - is NumericExpressionContext -> hasNextNumericImplicitMultExpressionRhs() - is AlgebraicExpressionContext -> hasNextAlgebraicImplicitMultOrExpExpressionRhs() - } - } - private fun parseGenericImplicitMultExpressionRhs(): MathParsingResult { // generic_implicit_mult_expression_rhs is either numeric_implicit_mult_expression_rhs or // algebraic_implicit_mult_or_exp_expression_rhs depending on the current parser context. @@ -283,17 +230,11 @@ class NumericExpressionParser private constructor( } } - private fun hasNextNumericImplicitMultExpressionRhs(): Boolean = - hasNextGenericTermWithoutUnaryWithoutNumber() - private fun parseNumericImplicitMultExpressionRhs(): MathParsingResult { // numeric_implicit_mult_expression_rhs = generic_term_without_unary_without_number ; return parseGenericTermWithoutUnaryWithoutNumber() } - private fun hasNextAlgebraicImplicitMultOrExpExpressionRhs(): Boolean = - hasNextGenericTermWithoutUnaryWithoutNumber() - private fun parseAlgebraicImplicitMultOrExpExpressionRhs(): MathParsingResult { // algebraic_implicit_mult_or_exp_expression_rhs = // generic_term_without_unary_without_number , [ generic_exp_expression_tail ] ; @@ -306,14 +247,11 @@ class NumericExpressionParser private constructor( private fun parseGenericExpExpression(): MathParsingResult { // generic_exp_expression = generic_term_with_unary , [ generic_exp_expression_tail ] ; val possibleLhs = parseGenericTermWithUnary() - return when { - hasNextGenericExpExpressionTail() -> parseGenericExpExpressionTail(possibleLhs) - else -> possibleLhs - } + return if (tokens.peek() is ExponentiationSymbol) { + parseGenericExpExpressionTail(possibleLhs) + } else possibleLhs } - private fun hasNextGenericExpExpressionTail(): Boolean = tokens.peek() is ExponentiationSymbol - // Use tail recursion so that the last exponentiation is evaluated first, and right-to-left // associativity can be kept via backtracking. private fun parseGenericExpExpressionTail( @@ -322,7 +260,7 @@ class NumericExpressionParser private constructor( // generic_exp_expression_tail = exponentiation_operator , generic_exp_expression ; val rhsResult = lhsResult.flatMap { - consumeTokenOfType { MathParsingError.GenericError } + consumeTokenOfType() }.flatMap { parseGenericExpExpression() } @@ -342,20 +280,21 @@ class NumericExpressionParser private constructor( private fun parseGenericTermWithUnary(): MathParsingResult { // generic_term_with_unary = // number | generic_term_without_unary_without_number | generic_plus_minus_unary_term ; - return when { - hasNextGenericPlusMinusUnaryTerm() -> parseGenericPlusMinusUnaryTerm() - hasNextNumber() -> parseNumber().takeUnless { - hasNextNumber() + return when (val nextToken = tokens.peek()) { + is MinusSymbol, is PlusSymbol -> parseGenericPlusMinusUnaryTerm() + is PositiveInteger, is PositiveRealNumber -> parseNumber().takeUnless { + tokens.peek() is PositiveInteger || tokens.peek() is PositiveRealNumber } ?: MathParsingError.SpacesBetweenNumbersError.toFailure() - hasNextGenericTermWithoutUnaryWithoutNumber() -> parseGenericTermWithoutUnaryWithoutNumber() - else -> MathParsingError.GenericError.toFailure() - } - } - - private fun hasNextGenericTermWithoutUnaryWithoutNumber(): Boolean { - return when (parseContext) { - is NumericExpressionContext -> hasNextNumericTermWithoutUnaryWithoutNumber() - is AlgebraicExpressionContext -> hasNextAlgebraicTermWithoutUnaryWithoutNumber() + is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> + parseGenericTermWithoutUnaryWithoutNumber() + is VariableName -> { + if (parseContext is AlgebraicExpressionContext) { + parseGenericTermWithoutUnaryWithoutNumber() + } else MathParsingError.VariableInNumericExpressionError.toFailure() + } + is DivideSymbol, is EqualsSymbol, is ExponentiationSymbol, is MultiplySymbol, + is RightParenthesisSymbol, null -> MathParsingError.GenericError.toFailure() + is InvalidToken -> nextToken.toFailure() } } @@ -368,53 +307,45 @@ class NumericExpressionParser private constructor( } } - private fun hasNextNumericTermWithoutUnaryWithoutNumber(): Boolean = - hasNextGenericFunctionExpression() - || hasNextGenericGroupExpression() - || hasNextGenericRootedTerm() - private fun parseNumericTermWithoutUnaryWithoutNumber(): MathParsingResult { // numeric_term_without_unary_without_number = // generic_function_expression | generic_group_expression | generic_rooted_term ; - return when { - hasNextGenericFunctionExpression() -> parseGenericFunctionExpression() - hasNextGenericGroupExpression() -> parseGenericGroupExpression() - hasNextGenericRootedTerm() -> parseGenericRootedTerm() - else -> MathParsingError.GenericError.toFailure() + return when (val nextToken = tokens.peek()) { + is FunctionName -> parseGenericFunctionExpression() + is LeftParenthesisSymbol -> parseGenericGroupExpression() + is SquareRootSymbol -> parseGenericRootedTerm() + is VariableName -> MathParsingError.VariableInNumericExpressionError.toFailure() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, + is RightParenthesisSymbol, null -> MathParsingError.GenericError.toFailure() + is InvalidToken -> nextToken.toFailure() } } - private fun hasNextAlgebraicTermWithoutUnaryWithoutNumber(): Boolean = - hasNextGenericFunctionExpression() - || hasNextGenericGroupExpression() - || hasNextGenericRootedTerm() - || hasNextVariable() - private fun parseAlgebraicTermWithoutUnaryWithoutNumber(): MathParsingResult { // algebraic_term_without_unary_without_number = // generic_function_expression | generic_group_expression | generic_rooted_term | variable ; - return when { - hasNextGenericFunctionExpression() -> parseGenericFunctionExpression() - hasNextGenericGroupExpression() -> parseGenericGroupExpression() - hasNextGenericRootedTerm() -> parseGenericRootedTerm() - hasNextVariable() -> parseVariable() - else -> MathParsingError.GenericError.toFailure() + return when (val nextToken = tokens.peek()) { + is FunctionName -> parseGenericFunctionExpression() + is LeftParenthesisSymbol -> parseGenericGroupExpression() + is SquareRootSymbol -> parseGenericRootedTerm() + is VariableName -> parseVariable() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, + is RightParenthesisSymbol, null -> MathParsingError.GenericError.toFailure() + is InvalidToken -> nextToken.toFailure() } } - private fun hasNextGenericFunctionExpression(): Boolean = tokens.peek() is FunctionName - private fun parseGenericFunctionExpression(): MathParsingResult { // generic_function_expression = function_name , left_paren , generic_expression , right_paren ; val funcNameResult = - consumeTokenOfType { - MathParsingError.GenericError - }.maybeFail { functionName -> + consumeTokenOfType().maybeFail { functionName -> if (functionName.parsedName != "sqrt") { MathParsingError.GenericError } else null }.also { - consumeTokenOfType { MathParsingError.GenericError } + consumeTokenOfType() } val argResult = funcNameResult.flatMap { parseGenericExpression() } val rightParenResult = @@ -433,14 +364,9 @@ class NumericExpressionParser private constructor( } } - private fun hasNextGenericGroupExpression(): Boolean = tokens.peek() is LeftParenthesisSymbol - private fun parseGenericGroupExpression(): MathParsingResult { // generic_group_expression = left_paren , generic_expression , right_paren ; - val leftParenResult = - consumeTokenOfType { - MathParsingError.GenericError - } + val leftParenResult = consumeTokenOfType() val expResult = leftParenResult.flatMap { if (tokens.hasNext()) { @@ -460,23 +386,22 @@ class NumericExpressionParser private constructor( } } - private fun hasNextGenericPlusMinusUnaryTerm(): Boolean = - hasNextGenericNegatedTerm() || hasNextGenericPositiveTerm() - private fun parseGenericPlusMinusUnaryTerm(): MathParsingResult { // generic_plus_minus_unary_term = generic_negated_term | generic_positive_term ; - return when { - hasNextGenericNegatedTerm() -> parseGenericNegatedTerm() - hasNextGenericPositiveTerm() -> parseGenericPositiveTerm() - else -> MathParsingError.GenericError.toFailure() + return when (val nextToken = tokens.peek()) { + is MinusSymbol -> parseGenericNegatedTerm() + is PlusSymbol -> parseGenericPositiveTerm() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is FunctionName, is LeftParenthesisSymbol, is MultiplySymbol, + is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, null -> + MathParsingError.GenericError.toFailure() + is InvalidToken -> nextToken.toFailure() } } - private fun hasNextGenericNegatedTerm(): Boolean = tokens.peek() is MinusSymbol - private fun parseGenericNegatedTerm(): MathParsingResult { // generic_negated_term = minus_operator , generic_mult_div_expression ; - val minusResult = consumeTokenOfType { MathParsingError.GenericError } + val minusResult = consumeTokenOfType() val expResult = minusResult.flatMap { parseGenericMultDivExpression() } return minusResult.combineWith(expResult) { minus, op -> MathExpression.newBuilder().apply { @@ -490,11 +415,9 @@ class NumericExpressionParser private constructor( } } - private fun hasNextGenericPositiveTerm(): Boolean = tokens.peek() is PlusSymbol - private fun parseGenericPositiveTerm(): MathParsingResult { // generic_positive_term = plus_operator , generic_mult_div_expression ; - val plusResult = consumeTokenOfType { MathParsingError.GenericError } + val plusResult = consumeTokenOfType() val expResult = plusResult.flatMap { parseGenericMultDivExpression() } return plusResult.combineWith(expResult) { plus, op -> MathExpression.newBuilder().apply { @@ -508,11 +431,12 @@ class NumericExpressionParser private constructor( } } - private fun hasNextGenericRootedTerm(): Boolean = tokens.peek() is SquareRootSymbol - private fun parseGenericRootedTerm(): MathParsingResult { // generic_rooted_term = square_root_operator , generic_term_with_unary ; - val sqrtResult = consumeTokenOfType { MathParsingError.GenericError } + val sqrtResult = + consumeTokenOfType().maybeFail { + if (!tokens.hasNext()) MathParsingError.HangingSquareRootError else null + } val expResult = sqrtResult.flatMap { parseGenericTermWithUnary() } return sqrtResult.combineWith(expResult) { sqrtSymbol, op -> MathExpression.newBuilder().apply { @@ -526,57 +450,51 @@ class NumericExpressionParser private constructor( } } - private fun hasNextNumber(): Boolean = hasNextPositiveInteger() || hasNextPositiveRealNumber() - private fun parseNumber(): MathParsingResult { // number = positive_real_number | positive_integer ; - return when { - hasNextPositiveInteger() -> { - consumeTokenOfType { - MathParsingError.GenericError - }.map { int -> + return when (val nextToken = tokens.peek()) { + is PositiveInteger -> { + consumeTokenOfType().map { positiveInteger -> MathExpression.newBuilder().apply { - parseStartIndex = int.startIndex - parseEndIndex = int.endIndex - constant = Real.newBuilder().apply { - integer = int.parsedValue - }.build() + parseStartIndex = positiveInteger.startIndex + parseEndIndex = positiveInteger.endIndex + constant = positiveInteger.toReal() }.build() } } - hasNextPositiveRealNumber() -> { - consumeTokenOfType { - MathParsingError.GenericError - }.map { real -> + is PositiveRealNumber -> { + consumeTokenOfType().map { positiveRealNumber -> MathExpression.newBuilder().apply { - parseStartIndex = real.startIndex - parseEndIndex = real.endIndex - constant = Real.newBuilder().apply { - irrational = real.parsedValue - }.build() + parseStartIndex = positiveRealNumber.startIndex + parseEndIndex = positiveRealNumber.endIndex + constant = positiveRealNumber.toReal() }.build() } } - // TODO: add error that one of the above was expected. Other error handling should maybe - // happen in the same way. - else -> MathParsingError.GenericError.toFailure() + is DivideSymbol, is EqualsSymbol, is ExponentiationSymbol, is FunctionName, + is LeftParenthesisSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, + is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, null -> + MathParsingError.GenericError.toFailure() + is InvalidToken -> nextToken.toFailure() } } - private fun hasNextPositiveInteger(): Boolean = tokens.peek() is PositiveInteger - - private fun hasNextPositiveRealNumber(): Boolean = tokens.peek() is PositiveRealNumber - - private fun hasNextVariable(): Boolean = tokens.peek() is VariableName - private fun parseVariable(): MathParsingResult { val variableNameResult = - consumeTokenOfType { - MathParsingError.GenericError - }.maybeFail { variableName -> + consumeTokenOfType().maybeFail { variableName -> if (!parseContext.allowsVariable(variableName.parsedName)) { MathParsingError.GenericError } else null + }.maybeFail { variableName -> + return@maybeFail if (tokens.hasNext()) { + when (val nextToken = tokens.peek()) { + is PositiveInteger -> + MathParsingError.NumberAfterVariableError(nextToken.toReal(), variableName.parsedName) + is PositiveRealNumber -> + MathParsingError.NumberAfterVariableError(nextToken.toReal(), variableName.parsedName) + else -> null + } + } else null } return variableNameResult.map { variableName -> MathExpression.newBuilder().apply { @@ -587,8 +505,16 @@ class NumericExpressionParser private constructor( } } + private fun PositiveInteger.toReal(): Real = Real.newBuilder().apply { + integer = parsedValue + }.build() + + private fun PositiveRealNumber.toReal(): Real = Real.newBuilder().apply { + irrational = parsedValue + }.build() + private inline fun consumeTokenOfType( - missingError: () -> MathParsingError + missingError: () -> MathParsingError = { MathParsingError.GenericError } ): MathParsingResult { val maybeToken = tokens.expectNextMatches { it is T } as? T return maybeToken?.let { token -> @@ -596,11 +522,24 @@ class NumericExpressionParser private constructor( } ?: missingError().toFailure() } + private fun InvalidToken.toError(): MathParsingError = + MathParsingError.UnnecessarySymbolsError(parseContext.extractSubexpression(this)) + + private fun InvalidToken.toFailure(): MathParsingResult = toError().toFailure() + private sealed class ParseContext(val rawExpression: String) { abstract val errorCheckingMode: ErrorCheckingMode abstract fun allowsVariable(variableName: String): Boolean + fun extractSubexpression(token: Token): String { + return rawExpression.substring(token.startIndex, token.endIndex) + } + + fun extractSubexpression(expression: MathExpression): String { + return rawExpression.substring(expression.parseStartIndex, expression.parseEndIndex) + } + class NumericExpressionContext( rawExpression: String, override val errorCheckingMode: ErrorCheckingMode ) : ParseContext(rawExpression) { @@ -786,4 +725,34 @@ class NumericExpressionParser private constructor( } } } + + private fun MathExpression.findFirstMultiRedundantGroup(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findFirstMultiRedundantGroup() + ?: binaryOperation.rightOperand.findFirstMultiRedundantGroup() + } + UNARY_OPERATION -> unaryOperation.operand.findFirstMultiRedundantGroup() + FUNCTION_CALL -> functionCall.argument.findFirstMultiRedundantGroup() + GROUP -> group.takeIf { it.expressionTypeCase == GROUP } + ?: group.findFirstMultiRedundantGroup() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextRedundantGroup(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findNextRedundantGroup() + ?: binaryOperation.rightOperand.findNextRedundantGroup() + } + UNARY_OPERATION -> unaryOperation.operand.findNextRedundantGroup() + FUNCTION_CALL -> functionCall.argument.findNextRedundantGroup() + GROUP -> { + group.takeIf { it.expressionTypeCase in listOf(CONSTANT, VARIABLE) } + ?: group.findNextRedundantGroup() + } + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } } diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 23a752194d2..402368c5f01 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -27,11 +27,16 @@ import org.robolectric.annotation.LooperMode import kotlin.math.sqrt import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError +import org.oppia.android.util.math.MathParsingError.HangingSquareRootError import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.NumberAfterVariableError import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError +import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError +import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError import org.oppia.android.util.math.NumericExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.NumericExpressionParser.Companion.MathParsingResult @@ -94,6 +99,51 @@ class NumericExpressionParserTest { val failure12 = expectFailureWhenParsingNumericExpression("sqrt((2))") assertThat(failure12).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + val failure16 = expectFailureWhenParsingNumericExpression("$2") + assertThat(failure16).isInstanceOf(UnnecessarySymbolsError::class.java) + assertThat((failure16 as UnnecessarySymbolsError).invalidSymbol).isEqualTo("$") + + val failure17 = expectFailureWhenParsingNumericExpression("5%") + assertThat(failure17).isInstanceOf(UnnecessarySymbolsError::class.java) + assertThat((failure17 as UnnecessarySymbolsError).invalidSymbol).isEqualTo("%") + + val failure18 = expectFailureWhenParsingAlgebraicExpression("x5") + assertThat(failure18).isInstanceOf(NumberAfterVariableError::class.java) + assertThat((failure18 as NumberAfterVariableError).number.integer).isEqualTo(5) + assertThat(failure18.variable).isEqualTo("x") + + val failure19 = expectFailureWhenParsingAlgebraicExpression("2+y 3.14*7") + assertThat(failure19).isInstanceOf(NumberAfterVariableError::class.java) + assertThat((failure19 as NumberAfterVariableError).number.irrational).isWithin(1e-5).of(3.14) + assertThat(failure19.variable).isEqualTo("y") + + // SubsequentBinaryOperatorsError + + // SubsequentUnaryOperatorsError -> analysis + + // NoVariableOrNumberBeforeBinaryOperatorError + // NoVariableOrNumberAfterBinaryOperatorError + + // ExponentIsVariableExpressionError -> analysis + // ExponentTooLargeError -> analysis + // NestedExponentsError -> analysis + + val failure20 = expectFailureWhenParsingNumericExpression("2√") + assertThat(failure20).isInstanceOf(HangingSquareRootError::class.java) + + // TermDividedByZeroError -> analysis + + val failure21 = expectFailureWhenParsingNumericExpression("x+y") + assertThat(failure21).isInstanceOf(VariableInNumericExpressionError::class.java) + + // DisabledVariablesInUseError -> analysis + + // EquationHasWrongNumberOfEqualsError + // EquationMissingLhsOrRhsError + // InvalidFunctionInUseError + + // FunctionNameUsedAsVariables -> analysis + // TODO: Other cases: sqrt(, sqrt(), sqrt 2 } @@ -4274,9 +4324,10 @@ class NumericExpressionParserTest { return NumericExpressionParser.parseNumericExpression(expression, errorCheckingMode) } - private fun expectFailureWhenParsingAlgebraicExpression(expression: String) { - assertThat(parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS)) - .isInstanceOf(MathParsingResult.Failure::class.java) + private fun expectFailureWhenParsingAlgebraicExpression(expression: String): MathParsingError { + val result = parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error } private fun parseAlgebraicExpressionWithoutOptionalErrors( From 877b003a9cc910922415855ef255f06f1eca5c60 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 18 Nov 2021 13:34:00 -0800 Subject: [PATCH 081/289] Refactor binary operator parsing. This is an overall reduction of 3 lines of code. I'm going to be trying a more orthodox method of refactoring in a subsequent commit to try and simplify the tokenization parts, so I'm committing this to snapshot this approach. --- .../util/math/NumericExpressionParser.kt | 121 +++++++++--------- 1 file changed, 60 insertions(+), 61 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt index 60f06b0b4c3..2d8ca55afc5 100644 --- a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt @@ -118,38 +118,26 @@ class NumericExpressionParser private constructor( return parseGenericAddSubExpression() } - // TODO: consider consolidating this with other binary parsing to reduce the overall parser. private fun parseGenericAddSubExpression(): MathParsingResult { // generic_add_sub_expression = // generic_mult_div_expression , { generic_add_sub_expression_rhs } ; - var lastLhsResult = parseGenericMultDivExpression() - while (!lastLhsResult.isFailure()) { - // generic_add_sub_expression_rhs = generic_add_expression_rhs | generic_sub_expression_rhs ; - val (operator, rhsResult) = when (tokens.peek()) { - is PlusSymbol -> - MathBinaryOperation.Operator.ADD to parseGenericAddExpressionRhs() - is MinusSymbol -> - MathBinaryOperation.Operator.SUBTRACT to parseGenericSubExpressionRhs() - is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, - is ExponentiationSymbol, is FunctionName, is InvalidToken, is LeftParenthesisSymbol, - is MultiplySymbol, is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, null -> - break // Not a match to the expression. - } - - lastLhsResult = lastLhsResult.combineWith(rhsResult) { lhs, rhs -> - // Compute the next LHS if there is further addition/subtraction. - MathExpression.newBuilder().apply { - parseStartIndex = lhs.parseStartIndex - parseEndIndex = rhs.parseEndIndex - binaryOperation = MathBinaryOperation.newBuilder().apply { - this.operator = operator - leftOperand = lhs - rightOperand = rhs - }.build() - }.build() + return parseGenericBinaryExpression( + parseLhs = this::parseGenericMultDivExpression, + parseRhs = { nextToken -> + // generic_add_sub_expression_rhs = + // generic_add_expression_rhs | generic_sub_expression_rhs ; + when (nextToken) { + is PlusSymbol -> + MathBinaryOperation.Operator.ADD to parseGenericAddExpressionRhs() + is MinusSymbol -> + MathBinaryOperation.Operator.SUBTRACT to parseGenericSubExpressionRhs() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is FunctionName, is InvalidToken, is LeftParenthesisSymbol, + is MultiplySymbol, is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, + null -> null + } } - } - return lastLhsResult + ) } private fun parseGenericAddExpressionRhs(): MathParsingResult { @@ -169,42 +157,30 @@ class NumericExpressionParser private constructor( private fun parseGenericMultDivExpression(): MathParsingResult { // generic_mult_div_expression = // generic_exp_expression , { generic_mult_div_expression_rhs } ; - var lastLhsResult = parseGenericExpExpression() - while (!lastLhsResult.isFailure()) { - // generic_mult_div_expression_rhs = - // generic_mult_expression_rhs - // | generic_div_expression_rhs - // | generic_implicit_mult_expression_rhs ; - val (operator, rhsResult) = when (tokens.peek()) { - is MultiplySymbol -> - MathBinaryOperation.Operator.MULTIPLY to parseGenericMultExpressionRhs() - is DivideSymbol -> MathBinaryOperation.Operator.DIVIDE to parseGenericDivExpressionRhs() - is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> - MathBinaryOperation.Operator.MULTIPLY to parseGenericImplicitMultExpressionRhs() - is VariableName -> { - if (parseContext is AlgebraicExpressionContext) { + return parseGenericBinaryExpression( + parseLhs = this::parseGenericExpExpression, + parseRhs = { nextToken -> + // generic_mult_div_expression_rhs = + // generic_mult_expression_rhs + // | generic_div_expression_rhs + // | generic_implicit_mult_expression_rhs ; + when (nextToken) { + is MultiplySymbol -> + MathBinaryOperation.Operator.MULTIPLY to parseGenericMultExpressionRhs() + is DivideSymbol -> MathBinaryOperation.Operator.DIVIDE to parseGenericDivExpressionRhs() + is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> MathBinaryOperation.Operator.MULTIPLY to parseGenericImplicitMultExpressionRhs() - } else break + is VariableName -> { + if (parseContext is AlgebraicExpressionContext) { + MathBinaryOperation.Operator.MULTIPLY to parseGenericImplicitMultExpressionRhs() + } else null + } + // Not a match to the expression. + is PositiveInteger, is PositiveRealNumber, is EqualsSymbol, is ExponentiationSymbol, + is InvalidToken, is MinusSymbol, is PlusSymbol, is RightParenthesisSymbol, null -> null } - // Not a match to the expression. - is PositiveInteger, is PositiveRealNumber, is EqualsSymbol, is ExponentiationSymbol, - is InvalidToken, is MinusSymbol, is PlusSymbol, is RightParenthesisSymbol, null -> break - } - - // Compute the next LHS if there is further multiplication/division. - lastLhsResult = lastLhsResult.combineWith(rhsResult) { lhs, rhs -> - MathExpression.newBuilder().apply { - parseStartIndex = lhs.parseStartIndex - parseEndIndex = rhs.parseEndIndex - binaryOperation = MathBinaryOperation.newBuilder().apply { - this.operator = operator - leftOperand = lhs - rightOperand = rhs - }.build() - }.build() } - } - return lastLhsResult + ) } private fun parseGenericMultExpressionRhs(): MathParsingResult { @@ -505,6 +481,29 @@ class NumericExpressionParser private constructor( } } + private fun parseGenericBinaryExpression( + parseLhs: () -> MathParsingResult, + parseRhs: (Token?) -> Pair>? + ): MathParsingResult { + var lastLhsResult = parseLhs() + while (!lastLhsResult.isFailure()) { + // Compute the next LHS if there are further RHS expressions. + val (operator, rhsResult) = parseRhs(tokens.peek()) ?: break // Not a match to the expression. + lastLhsResult = lastLhsResult.combineWith(rhsResult) { lhs, rhs -> + MathExpression.newBuilder().apply { + parseStartIndex = lhs.parseStartIndex + parseEndIndex = rhs.parseEndIndex + binaryOperation = MathBinaryOperation.newBuilder().apply { + this.operator = operator + leftOperand = lhs + rightOperand = rhs + }.build() + }.build() + } + } + return lastLhsResult + } + private fun PositiveInteger.toReal(): Real = Real.newBuilder().apply { integer = parsedValue }.build() From f013668e89cfbe1898a2138ea7557ca3ab2f7d4e Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 18 Nov 2021 22:38:54 -0800 Subject: [PATCH 082/289] Add all remaining tests per the doc. --- .../org/oppia/android/util/math/BUILD.bazel | 2 + .../util/math/MathExpressionExtensions.kt | 2 +- .../android/util/math/MathParsingError.kt | 7 +- .../oppia/android/util/math/MathTokenizer2.kt | 211 +++++- .../util/math/NumericExpressionParser.kt | 660 ++++++++++++------ .../util/math/NumericExpressionParserTest.kt | 194 ++++- 6 files changed, 834 insertions(+), 242 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index de64a550f4f..f447a817474 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -42,6 +42,7 @@ kt_android_library( "//:oppia_testing_visibility", ], deps = [ + ":extensions", ":math_parsing_error", ":peekable_iterator", ":tokenizer", @@ -60,6 +61,7 @@ kt_android_library( ], deps = [ ":peekable_iterator", + "//model/src/main/proto:math_java_proto_lite", ], ) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index a78c28e4597..cd322b69b6e 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -487,7 +487,7 @@ private fun Real.isApproximatelyEqualTo(value: Double): Boolean { private fun Real.isApproximatelyZero(): Boolean = isApproximatelyEqualTo(0.0) -private fun Real.toDouble(): Double { +fun Real.toDouble(): Double { return when (realTypeCase) { RATIONAL -> rational.toDouble() INTEGER -> integer.toDouble() diff --git a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt index e7af82d3c47..24c1c28d242 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt @@ -1,7 +1,6 @@ package org.oppia.android.util.math import org.oppia.android.app.model.MathBinaryOperation -import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression import org.oppia.android.app.model.Real @@ -34,11 +33,11 @@ sealed class MathParsingError { object SubsequentUnaryOperatorsError : MathParsingError() data class NoVariableOrNumberBeforeBinaryOperatorError( - val operator: MathBinaryOperation.Operator, val operatorSymbol: String + val operator: MathBinaryOperation.Operator ): MathParsingError() data class NoVariableOrNumberAfterBinaryOperatorError( - val operator: MathBinaryOperation.Operator, val operatorSymbol: String + val operator: MathBinaryOperation.Operator ): MathParsingError() object ExponentIsVariableExpressionError : MathParsingError() @@ -61,7 +60,7 @@ sealed class MathParsingError { data class InvalidFunctionInUseError(val functionName: String) : MathParsingError() - data class FunctionNameUsedAsVariables(val expectedFunctionName: String) : MathParsingError() + object FunctionNameIncompleteError : MathParsingError() object GenericError : MathParsingError() } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt index f3622ed3ff0..9f9ba15934d 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt @@ -1,6 +1,15 @@ package org.oppia.android.util.math import java.lang.StringBuilder +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE // TODO: rename to MathTokenizer & add documentation. // TODO: consider a more efficient implementation that uses 1 underlying buffer (which could still @@ -90,21 +99,135 @@ class MathTokenizer2 private constructor() { private fun tokenizeVariableOrFunctionName(chars: PeekableIterator): Token { val startIndex = chars.getRetrievalCount() val firstChar = chars.next() + + // latin_letter = lowercase_latin_letter | uppercase_latin_letter ; + // variable = latin_letter ; + return tokenizeFunctionName(firstChar, startIndex, chars) + ?: Token.VariableName( + firstChar.toString(), startIndex, endIndex = chars.getRetrievalCount() + ) + } + + private fun tokenizeFunctionName( + currChar: Char, startIndex: Int, chars: PeekableIterator + ): Token? { + // allowed_function_name = "sqrt" ; + // disallowed_function_name = + // "exp" | "log" | "log10" | "ln" | "sin" | "cos" | "tan" | "cot" | "csc" + // | "sec" | "atan" | "asin" | "acos" | "abs" ; + // function_name = allowed_function_name | disallowed_function_name ; val nextChar = chars.peek() - return if (firstChar == 's' && nextChar == 'q') { - // With 'sq' next to each other, 'rt' is expected to follow. - chars.expectNextValue { 'q' } - ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) - chars.expectNextValue { 'r' } - ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) - chars.expectNextValue { 't' } - ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) - Token.FunctionName("sqrt", startIndex, endIndex = chars.getRetrievalCount()) - } else { - Token.VariableName(firstChar.toString(), startIndex, endIndex = chars.getRetrievalCount()) + return when (currChar) { + 'a' -> { + // abs, acos, asin, atan, or variable. + when (nextChar) { + 'b' -> + tokenizeExpectedFunction(name = "abs", isAllowedFunction = false, startIndex, chars) + 'c' -> + tokenizeExpectedFunction(name = "acos", isAllowedFunction = false, startIndex, chars) + 's' -> + tokenizeExpectedFunction(name = "asin", isAllowedFunction = false, startIndex, chars) + 't' -> + tokenizeExpectedFunction(name = "atan", isAllowedFunction = false, startIndex, chars) + else -> null // Must be a variable. + } + } + 'c' -> { + // cos, cot, csc, or variable. + when (nextChar) { + 'o' -> { + chars.next() // Skip the 'o' to go to the last character. + val name = if (chars.peek() == 's') { + chars.expectNextMatches { it == 's' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + "cos" + } else { + // Otherwise, it must be 'c' for 'cot' since the parser can't backtrack. + chars.expectNextMatches { it == 't' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + "cot" + } + Token.FunctionName( + name, isAllowedFunction = false, startIndex, endIndex = chars.getRetrievalCount() + ) + } + 's' -> + tokenizeExpectedFunction(name = "csc", isAllowedFunction = false, startIndex, chars) + else -> null // Must be a variable. + } + } + 'e' -> { + // exp or variable. + if (nextChar == 'x') { + tokenizeExpectedFunction(name = "exp", isAllowedFunction = false, startIndex, chars) + } else null // Must be a variable. + } + 'l' -> { + // ln, log, log10, or variable. + when (nextChar) { + 'n' -> + tokenizeExpectedFunction(name = "ln", isAllowedFunction = false, startIndex, chars) + 'o' -> { + // Skip the 'o'. Following the 'o' must be a 'g' since the parser can't backtrack. + chars.next() + chars.expectNextMatches { it == 'g' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + val name = if (chars.peek() == '1') { + // '10' must be next for 'log10'. + chars.expectNextMatches { it == '1' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + chars.expectNextMatches { it == '0' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + "log10" + } else "log" + Token.FunctionName( + name, isAllowedFunction = false, startIndex, endIndex = chars.getRetrievalCount() + ) + } + else -> null // Must be a variable. + } + } + 's' -> { + // sec, sin, sqrt, or variable. + when (nextChar) { + 'e' -> + tokenizeExpectedFunction(name = "sec", isAllowedFunction = false, startIndex, chars) + 'i' -> + tokenizeExpectedFunction(name = "sin", isAllowedFunction = false, startIndex, chars) + 'q' -> + tokenizeExpectedFunction(name = "sqrt", isAllowedFunction = true, startIndex, chars) + else -> null // Must be a variable. + } + } + 't' -> { + // tan or variable. + if (nextChar == 'a') { + tokenizeExpectedFunction(name = "tan", isAllowedFunction = false, startIndex, chars) + } else null // Must be a variable. + } + else -> null // Must be a variable since no known functions match the first character. } } + private fun tokenizeExpectedFunction( + name: String, isAllowedFunction: Boolean, startIndex: Int, chars: PeekableIterator + ): Token { + return chars.expectNextCharsForFunctionName(name.substring(1), startIndex) + ?: Token.FunctionName( + name, isAllowedFunction, startIndex, endIndex = chars.getRetrievalCount() + ) + } + private fun tokenizeSymbol(chars: PeekableIterator, factory: (Int, Int) -> Token): Token { val startIndex = chars.getRetrievalCount() chars.next() // Parse the symbol. @@ -122,6 +245,14 @@ class MathTokenizer2 private constructor() { } else null // Failed to parse; no digits. } + interface UnaryOperatorToken { + fun getUnaryOperator(): MathUnaryOperation.Operator + } + + interface BinaryOperatorToken { + fun getBinaryOperator(): MathBinaryOperation.Operator + } + sealed class Token { /** The index in the input stream at which point this token begins. */ abstract val startIndex: Int @@ -142,20 +273,45 @@ class MathTokenizer2 private constructor() { ) : Token() class FunctionName( - val parsedName: String, override val startIndex: Int, override val endIndex: Int + val parsedName: String, val isAllowedFunction: Boolean, override val startIndex: Int, + override val endIndex: Int ) : Token() - class MinusSymbol(override val startIndex: Int, override val endIndex: Int) : Token() + class MinusSymbol( + override val startIndex: Int, override val endIndex: Int + ) : Token(), UnaryOperatorToken, BinaryOperatorToken { + override fun getUnaryOperator(): MathUnaryOperation.Operator = NEGATE + + override fun getBinaryOperator(): MathBinaryOperation.Operator = SUBTRACT + } class SquareRootSymbol(override val startIndex: Int, override val endIndex: Int) : Token() - class PlusSymbol(override val startIndex: Int, override val endIndex: Int) : Token() + class PlusSymbol( + override val startIndex: Int, override val endIndex: Int + ) : Token(), UnaryOperatorToken, BinaryOperatorToken { + override fun getUnaryOperator(): MathUnaryOperation.Operator = POSITIVE - class MultiplySymbol(override val startIndex: Int, override val endIndex: Int) : Token() + override fun getBinaryOperator(): MathBinaryOperation.Operator = ADD + } + + class MultiplySymbol( + override val startIndex: Int, override val endIndex: Int + ) : Token(), BinaryOperatorToken { + override fun getBinaryOperator(): MathBinaryOperation.Operator = MULTIPLY + } - class DivideSymbol(override val startIndex: Int, override val endIndex: Int) : Token() + class DivideSymbol( + override val startIndex: Int, override val endIndex: Int + ) : Token(), BinaryOperatorToken { + override fun getBinaryOperator(): MathBinaryOperation.Operator = DIVIDE + } - class ExponentiationSymbol(override val startIndex: Int, override val endIndex: Int) : Token() + class ExponentiationSymbol( + override val startIndex: Int, override val endIndex: Int + ) : Token(), BinaryOperatorToken { + override fun getBinaryOperator(): MathBinaryOperation.Operator = EXPONENTIATE + } class EqualsSymbol(override val startIndex: Int, override val endIndex: Int) : Token() @@ -167,7 +323,10 @@ class MathTokenizer2 private constructor() { override val startIndex: Int, override val endIndex: Int ) : Token() - // TODO: add context to line & index, and enum for context on failure. + class IncompleteFunctionName( + override val startIndex: Int, override val endIndex: Int + ) : Token() + class InvalidToken(override val startIndex: Int, override val endIndex: Int) : Token() } @@ -180,5 +339,21 @@ class MathTokenizer2 private constructor() { private fun PeekableIterator.consumeWhitespace() { while (peek()?.isWhitespace() == true) next() } + + /** + * Expects each of the characters to be next in the token stream, in the order of the string. + * All characters must be present in [this] iterator. Returns non-null if a failure occurs, + * otherwise null if all characters were confirmed to be present. If null is returned, [this] + * iterator will be at the token that comes after the last confirmed character in the string. + */ + private fun PeekableIterator.expectNextCharsForFunctionName( + chars: String, startIndex: Int + ): Token? { + for (c in chars) { + expectNextValue { c } + ?: return Token.IncompleteFunctionName(startIndex, endIndex = getRetrievalCount()) + } + return null + } } } diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt index 2d8ca55afc5..067687da5f5 100644 --- a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt @@ -1,6 +1,12 @@ package org.oppia.android.util.math +import kotlin.math.absoluteValue import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION @@ -12,12 +18,38 @@ import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERA import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.MathFunctionCall import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE import org.oppia.android.app.model.Real +import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError +import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError +import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError +import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError +import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError +import org.oppia.android.util.math.MathParsingError.FunctionNameIncompleteError +import org.oppia.android.util.math.MathParsingError.GenericError +import org.oppia.android.util.math.MathParsingError.HangingSquareRootError +import org.oppia.android.util.math.MathParsingError.InvalidFunctionInUseError +import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.NestedExponentsError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberAfterBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberBeforeBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NumberAfterVariableError +import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError +import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError +import org.oppia.android.util.math.MathParsingError.SubsequentBinaryOperatorsError +import org.oppia.android.util.math.MathParsingError.SubsequentUnaryOperatorsError +import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError +import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError +import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError +import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError import org.oppia.android.util.math.MathTokenizer2.Companion.Token import org.oppia.android.util.math.MathTokenizer2.Companion.Token.DivideSymbol import org.oppia.android.util.math.MathTokenizer2.Companion.Token.EqualsSymbol import org.oppia.android.util.math.MathTokenizer2.Companion.Token.ExponentiationSymbol import org.oppia.android.util.math.MathTokenizer2.Companion.Token.FunctionName +import org.oppia.android.util.math.MathTokenizer2.Companion.Token.IncompleteFunctionName import org.oppia.android.util.math.MathTokenizer2.Companion.Token.InvalidToken import org.oppia.android.util.math.MathTokenizer2.Companion.Token.LeftParenthesisSymbol import org.oppia.android.util.math.MathTokenizer2.Companion.Token.MinusSymbol @@ -31,14 +63,7 @@ import org.oppia.android.util.math.MathTokenizer2.Companion.Token.VariableName import org.oppia.android.util.math.NumericExpressionParser.ParseContext.AlgebraicExpressionContext import org.oppia.android.util.math.NumericExpressionParser.ParseContext.NumericExpressionContext -class NumericExpressionParser private constructor( - private val rawExpression: String, - private val parseContext: ParseContext -) { - private val tokens: PeekableIterator by lazy { - PeekableIterator.fromSequence(MathTokenizer2.tokenize(rawExpression)) - } - +class NumericExpressionParser private constructor(private val parseContext: ParseContext) { // TODO: // - Add helpers to reduce overall parser length. // - Integrate with new errors & update the routines to not rely on exceptions except in actual exceptional cases. Make sure optional errors can be disabled (for testing purposes). @@ -48,6 +73,7 @@ class NumericExpressionParser private constructor( // TODO: verify remaining GenericErrors are correct. // TODO: document that 'generic' means either 'numeric' or 'algebraic' (ie that the expression is syntactically the same between both grammars). + // TODO: document that one design goal is keeping the grammar for this parser as LL(1) & why. private fun parseGenericEquationGrammar(): MathParsingResult { // generic_equation_grammar = generic_equation ; @@ -61,49 +87,23 @@ class NumericExpressionParser private constructor( return parseGenericExpression().maybeFail { expression -> checkForLearnerErrors(expression) } } - private fun checkForLearnerErrors(expression: MathExpression): MathParsingError? { - val firstMultiRedundantGroup = expression.findFirstMultiRedundantGroup() - val nextRedundantGroup = expression.findNextRedundantGroup() - // Note that the order of checks here is important since errors have precedence, and some are - // redundant and, in the wrong order, may cause the wrong error to be returned. - val includeOptionalErrors = parseContext.errorCheckingMode.includesOptionalErrors() - return when { - includeOptionalErrors && firstMultiRedundantGroup != null -> { - val subExpression = parseContext.extractSubexpression(firstMultiRedundantGroup) - MathParsingError.MultipleRedundantParenthesesError(subExpression, firstMultiRedundantGroup) - } - includeOptionalErrors && expression.expressionTypeCase == GROUP -> - MathParsingError.SingleRedundantParenthesesError(parseContext.rawExpression, expression) - includeOptionalErrors && nextRedundantGroup != null -> { - val subExpression = parseContext.extractSubexpression(nextRedundantGroup) - MathParsingError.RedundantParenthesesForIndividualTermsError( - subExpression, nextRedundantGroup - ) - } - else -> ensureNoRemainingTokens() - } - } - - private fun ensureNoRemainingTokens(): MathParsingError? { - // Make sure all tokens were consumed (otherwise there are trailing tokens which invalidate the - // whole grammar). - return if (tokens.hasNext()) { - when (val nextToken = tokens.peek()) { - is LeftParenthesisSymbol, is RightParenthesisSymbol -> - MathParsingError.UnbalancedParenthesesError - is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, - is ExponentiationSymbol, is FunctionName, is MinusSymbol, is MultiplySymbol, is PlusSymbol, - is SquareRootSymbol, is VariableName, null -> MathParsingError.GenericError - is InvalidToken -> nextToken.toError() - } - } else null - } - private fun parseGenericEquation(): MathParsingResult { // algebraic_equation = generic_expression , equals_operator , generic_expression ; + + if (parseContext.hasNextTokenOfType()) { + // If equals starts the string, then there's no LHS. + return EquationMissingLhsOrRhsError.toFailure() + } + val lhsResult = parseGenericExpression().also { - consumeTokenOfType() + parseContext.consumeTokenOfType() + }.maybeFail { + if (!parseContext.hasMoreTokens()) { + // If there are no tokens following the equals symbol, then there's no RHS. + EquationMissingLhsOrRhsError + } else null } + val rhsResult = lhsResult.flatMap { parseGenericExpression() } return lhsResult.combineWith(rhsResult) { lhs, rhs -> MathEquation.newBuilder().apply { @@ -122,34 +122,39 @@ class NumericExpressionParser private constructor( // generic_add_sub_expression = // generic_mult_div_expression , { generic_add_sub_expression_rhs } ; return parseGenericBinaryExpression( - parseLhs = this::parseGenericMultDivExpression, - parseRhs = { nextToken -> - // generic_add_sub_expression_rhs = - // generic_add_expression_rhs | generic_sub_expression_rhs ; - when (nextToken) { - is PlusSymbol -> - MathBinaryOperation.Operator.ADD to parseGenericAddExpressionRhs() - is MinusSymbol -> - MathBinaryOperation.Operator.SUBTRACT to parseGenericSubExpressionRhs() - is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, - is ExponentiationSymbol, is FunctionName, is InvalidToken, is LeftParenthesisSymbol, - is MultiplySymbol, is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, - null -> null - } + parseLhs = this::parseGenericMultDivExpression + ) { nextToken -> + // generic_add_sub_expression_rhs = + // generic_add_expression_rhs | generic_sub_expression_rhs ; + when (nextToken) { + is PlusSymbol -> ADD to parseGenericAddExpressionRhs() + is MinusSymbol -> SUBTRACT to parseGenericSubExpressionRhs() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is FunctionName, is InvalidToken, is LeftParenthesisSymbol, + is MultiplySymbol, is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, + is IncompleteFunctionName, null -> null } - ) + } } private fun parseGenericAddExpressionRhs(): MathParsingResult { // generic_add_expression_rhs = plus_operator , generic_mult_div_expression ; - return consumeTokenOfType().flatMap { + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(ADD) + } else null + }.flatMap { parseGenericMultDivExpression() } } private fun parseGenericSubExpressionRhs(): MathParsingResult { // generic_sub_expression_rhs = minus_operator , generic_mult_div_expression ; - return consumeTokenOfType().flatMap { + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(SUBTRACT) + } else null + }.flatMap { parseGenericMultDivExpression() } } @@ -158,41 +163,48 @@ class NumericExpressionParser private constructor( // generic_mult_div_expression = // generic_exp_expression , { generic_mult_div_expression_rhs } ; return parseGenericBinaryExpression( - parseLhs = this::parseGenericExpExpression, - parseRhs = { nextToken -> - // generic_mult_div_expression_rhs = - // generic_mult_expression_rhs - // | generic_div_expression_rhs - // | generic_implicit_mult_expression_rhs ; - when (nextToken) { - is MultiplySymbol -> - MathBinaryOperation.Operator.MULTIPLY to parseGenericMultExpressionRhs() - is DivideSymbol -> MathBinaryOperation.Operator.DIVIDE to parseGenericDivExpressionRhs() - is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> - MathBinaryOperation.Operator.MULTIPLY to parseGenericImplicitMultExpressionRhs() - is VariableName -> { - if (parseContext is AlgebraicExpressionContext) { - MathBinaryOperation.Operator.MULTIPLY to parseGenericImplicitMultExpressionRhs() - } else null - } - // Not a match to the expression. - is PositiveInteger, is PositiveRealNumber, is EqualsSymbol, is ExponentiationSymbol, - is InvalidToken, is MinusSymbol, is PlusSymbol, is RightParenthesisSymbol, null -> null + parseLhs = this::parseGenericExpExpression + ) { nextToken -> + // generic_mult_div_expression_rhs = + // generic_mult_expression_rhs + // | generic_div_expression_rhs + // | generic_implicit_mult_expression_rhs ; + when (nextToken) { + is MultiplySymbol -> MULTIPLY to parseGenericMultExpressionRhs() + is DivideSymbol -> DIVIDE to parseGenericDivExpressionRhs() + is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> + MULTIPLY to parseGenericImplicitMultExpressionRhs() + is VariableName -> { + if (parseContext is AlgebraicExpressionContext) { + MULTIPLY to parseGenericImplicitMultExpressionRhs() + } else null } + // Not a match to the expression. + is PositiveInteger, is PositiveRealNumber, is EqualsSymbol, is ExponentiationSymbol, + is InvalidToken, is MinusSymbol, is PlusSymbol, is RightParenthesisSymbol, + is IncompleteFunctionName, null -> null } - ) + } } private fun parseGenericMultExpressionRhs(): MathParsingResult { // generic_mult_expression_rhs = multiplication_operator , generic_exp_expression ; - return consumeTokenOfType().flatMap { + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(MULTIPLY) + } else null + }.flatMap { parseGenericExpExpression() } } private fun parseGenericDivExpressionRhs(): MathParsingResult { // generic_div_expression_rhs = division_operator , generic_exp_expression ; - return consumeTokenOfType().flatMap { + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(DIVIDE) + } else null + }.flatMap { parseGenericExpExpression() } } @@ -215,7 +227,7 @@ class NumericExpressionParser private constructor( // algebraic_implicit_mult_or_exp_expression_rhs = // generic_term_without_unary_without_number , [ generic_exp_expression_tail ] ; val possibleLhs = parseGenericTermWithoutUnaryWithoutNumber() - return if (tokens.peek() is ExponentiationSymbol) { + return if (parseContext.hasNextTokenOfType()) { parseGenericExpExpressionTail(possibleLhs) } else possibleLhs } @@ -223,7 +235,7 @@ class NumericExpressionParser private constructor( private fun parseGenericExpExpression(): MathParsingResult { // generic_exp_expression = generic_term_with_unary , [ generic_exp_expression_tail ] ; val possibleLhs = parseGenericTermWithUnary() - return if (tokens.peek() is ExponentiationSymbol) { + return if (parseContext.hasNextTokenOfType()) { parseGenericExpExpressionTail(possibleLhs) } else possibleLhs } @@ -233,44 +245,63 @@ class NumericExpressionParser private constructor( private fun parseGenericExpExpressionTail( lhsResult: MathParsingResult ): MathParsingResult { - // generic_exp_expression_tail = exponentiation_operator , generic_exp_expression ; - val rhsResult = - lhsResult.flatMap { - consumeTokenOfType() + return computeBinaryOperationExpression( + lhsResult + ) { + // generic_exp_expression_tail = exponentiation_operator , generic_exp_expression ; + EXPONENTIATE to lhsResult.flatMap { + parseContext.consumeTokenOfType() + }.maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(EXPONENTIATE) + } else null }.flatMap { parseGenericExpExpression() } - return lhsResult.combineWith(rhsResult) { lhs, rhs -> - MathExpression.newBuilder().apply { - parseStartIndex = lhs.parseStartIndex - parseEndIndex = rhs.parseEndIndex - binaryOperation = MathBinaryOperation.newBuilder().apply { - operator = MathBinaryOperation.Operator.EXPONENTIATE - leftOperand = lhs - rightOperand = rhs - }.build() - }.build() - } + } ?: GenericError.toFailure() } private fun parseGenericTermWithUnary(): MathParsingResult { // generic_term_with_unary = // number | generic_term_without_unary_without_number | generic_plus_minus_unary_term ; - return when (val nextToken = tokens.peek()) { + return when (val nextToken = parseContext.peekToken()) { is MinusSymbol, is PlusSymbol -> parseGenericPlusMinusUnaryTerm() is PositiveInteger, is PositiveRealNumber -> parseNumber().takeUnless { - tokens.peek() is PositiveInteger || tokens.peek() is PositiveRealNumber - } ?: MathParsingError.SpacesBetweenNumbersError.toFailure() + parseContext.hasNextTokenOfType() + || parseContext.hasNextTokenOfType() + } ?: SpacesBetweenNumbersError.toFailure() is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> parseGenericTermWithoutUnaryWithoutNumber() is VariableName -> { if (parseContext is AlgebraicExpressionContext) { parseGenericTermWithoutUnaryWithoutNumber() - } else MathParsingError.VariableInNumericExpressionError.toFailure() + } else VariableInNumericExpressionError.toFailure() + } + is DivideSymbol, is ExponentiationSymbol, is MultiplySymbol -> { + val previousToken = parseContext.getPreviousToken() + when { + previousToken is MathTokenizer2.Companion.BinaryOperatorToken -> { + SubsequentBinaryOperatorsError( + operator1 = parseContext.extractSubexpression(previousToken), + operator2 = parseContext.extractSubexpression(nextToken) + ).toFailure() + } + nextToken is MathTokenizer2.Companion.BinaryOperatorToken -> { + NoVariableOrNumberBeforeBinaryOperatorError( + operator = nextToken.getBinaryOperator() + ).toFailure() + } + else -> GenericError.toFailure() + } } - is DivideSymbol, is EqualsSymbol, is ExponentiationSymbol, is MultiplySymbol, - is RightParenthesisSymbol, null -> MathParsingError.GenericError.toFailure() + is EqualsSymbol -> { + if (parseContext is AlgebraicExpressionContext && parseContext.isPartOfEquation) { + EquationHasWrongNumberOfEqualsError.toFailure() + } else GenericError.toFailure() + } + is IncompleteFunctionName -> nextToken.toFailure() is InvalidToken -> nextToken.toFailure() + is RightParenthesisSymbol, null -> GenericError.toFailure() } } @@ -286,14 +317,15 @@ class NumericExpressionParser private constructor( private fun parseNumericTermWithoutUnaryWithoutNumber(): MathParsingResult { // numeric_term_without_unary_without_number = // generic_function_expression | generic_group_expression | generic_rooted_term ; - return when (val nextToken = tokens.peek()) { + return when (val nextToken = parseContext.peekToken()) { is FunctionName -> parseGenericFunctionExpression() is LeftParenthesisSymbol -> parseGenericGroupExpression() is SquareRootSymbol -> parseGenericRootedTerm() - is VariableName -> MathParsingError.VariableInNumericExpressionError.toFailure() + is VariableName -> VariableInNumericExpressionError.toFailure() is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, is ExponentiationSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, - is RightParenthesisSymbol, null -> MathParsingError.GenericError.toFailure() + is RightParenthesisSymbol, null -> GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() is InvalidToken -> nextToken.toFailure() } } @@ -301,14 +333,15 @@ class NumericExpressionParser private constructor( private fun parseAlgebraicTermWithoutUnaryWithoutNumber(): MathParsingResult { // algebraic_term_without_unary_without_number = // generic_function_expression | generic_group_expression | generic_rooted_term | variable ; - return when (val nextToken = tokens.peek()) { + return when (val nextToken = parseContext.peekToken()) { is FunctionName -> parseGenericFunctionExpression() is LeftParenthesisSymbol -> parseGenericGroupExpression() is SquareRootSymbol -> parseGenericRootedTerm() is VariableName -> parseVariable() is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, is ExponentiationSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, - is RightParenthesisSymbol, null -> MathParsingError.GenericError.toFailure() + is RightParenthesisSymbol, null -> GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() is InvalidToken -> nextToken.toFailure() } } @@ -316,17 +349,19 @@ class NumericExpressionParser private constructor( private fun parseGenericFunctionExpression(): MathParsingResult { // generic_function_expression = function_name , left_paren , generic_expression , right_paren ; val funcNameResult = - consumeTokenOfType().maybeFail { functionName -> - if (functionName.parsedName != "sqrt") { - MathParsingError.GenericError - } else null + parseContext.consumeTokenOfType().maybeFail { functionName -> + when { + !functionName.isAllowedFunction -> InvalidFunctionInUseError(functionName.parsedName) + functionName.parsedName == "sqrt" -> null + else -> GenericError + } }.also { - consumeTokenOfType() + parseContext.consumeTokenOfType() } val argResult = funcNameResult.flatMap { parseGenericExpression() } val rightParenResult = argResult.flatMap { - consumeTokenOfType { MathParsingError.UnbalancedParenthesesError } + parseContext.consumeTokenOfType { UnbalancedParenthesesError } } return funcNameResult.combineWith(argResult, rightParenResult) { funcName, arg, rightParen -> MathExpression.newBuilder().apply { @@ -342,16 +377,16 @@ class NumericExpressionParser private constructor( private fun parseGenericGroupExpression(): MathParsingResult { // generic_group_expression = left_paren , generic_expression , right_paren ; - val leftParenResult = consumeTokenOfType() + val leftParenResult = parseContext.consumeTokenOfType() val expResult = leftParenResult.flatMap { - if (tokens.hasNext()) { + if (parseContext.hasMoreTokens()) { parseGenericExpression() - } else MathParsingError.UnbalancedParenthesesError.toFailure() + } else UnbalancedParenthesesError.toFailure() } val rightParenResult = expResult.flatMap { - consumeTokenOfType { MathParsingError.UnbalancedParenthesesError } + parseContext.consumeTokenOfType { UnbalancedParenthesesError } } return leftParenResult.combineWith(expResult, rightParenResult) { leftParen, exp, rightParen -> MathExpression.newBuilder().apply { @@ -364,27 +399,28 @@ class NumericExpressionParser private constructor( private fun parseGenericPlusMinusUnaryTerm(): MathParsingResult { // generic_plus_minus_unary_term = generic_negated_term | generic_positive_term ; - return when (val nextToken = tokens.peek()) { + return when (val nextToken = parseContext.peekToken()) { is MinusSymbol -> parseGenericNegatedTerm() is PlusSymbol -> parseGenericPositiveTerm() is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, is ExponentiationSymbol, is FunctionName, is LeftParenthesisSymbol, is MultiplySymbol, is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, null -> - MathParsingError.GenericError.toFailure() + GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() is InvalidToken -> nextToken.toFailure() } } private fun parseGenericNegatedTerm(): MathParsingResult { // generic_negated_term = minus_operator , generic_mult_div_expression ; - val minusResult = consumeTokenOfType() + val minusResult = parseContext.consumeTokenOfType() val expResult = minusResult.flatMap { parseGenericMultDivExpression() } return minusResult.combineWith(expResult) { minus, op -> MathExpression.newBuilder().apply { parseStartIndex = minus.startIndex parseEndIndex = op.parseEndIndex unaryOperation = MathUnaryOperation.newBuilder().apply { - operator = MathUnaryOperation.Operator.NEGATE + operator = NEGATE operand = op }.build() }.build() @@ -393,14 +429,14 @@ class NumericExpressionParser private constructor( private fun parseGenericPositiveTerm(): MathParsingResult { // generic_positive_term = plus_operator , generic_mult_div_expression ; - val plusResult = consumeTokenOfType() + val plusResult = parseContext.consumeTokenOfType() val expResult = plusResult.flatMap { parseGenericMultDivExpression() } return plusResult.combineWith(expResult) { plus, op -> MathExpression.newBuilder().apply { parseStartIndex = plus.startIndex parseEndIndex = op.parseEndIndex unaryOperation = MathUnaryOperation.newBuilder().apply { - operator = MathUnaryOperation.Operator.POSITIVE + operator = POSITIVE operand = op }.build() }.build() @@ -410,8 +446,8 @@ class NumericExpressionParser private constructor( private fun parseGenericRootedTerm(): MathParsingResult { // generic_rooted_term = square_root_operator , generic_term_with_unary ; val sqrtResult = - consumeTokenOfType().maybeFail { - if (!tokens.hasNext()) MathParsingError.HangingSquareRootError else null + parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) HangingSquareRootError else null } val expResult = sqrtResult.flatMap { parseGenericTermWithUnary() } return sqrtResult.combineWith(expResult) { sqrtSymbol, op -> @@ -428,9 +464,9 @@ class NumericExpressionParser private constructor( private fun parseNumber(): MathParsingResult { // number = positive_real_number | positive_integer ; - return when (val nextToken = tokens.peek()) { + return when (val nextToken = parseContext.peekToken()) { is PositiveInteger -> { - consumeTokenOfType().map { positiveInteger -> + parseContext.consumeTokenOfType().map { positiveInteger -> MathExpression.newBuilder().apply { parseStartIndex = positiveInteger.startIndex parseEndIndex = positiveInteger.endIndex @@ -439,7 +475,7 @@ class NumericExpressionParser private constructor( } } is PositiveRealNumber -> { - consumeTokenOfType().map { positiveRealNumber -> + parseContext.consumeTokenOfType().map { positiveRealNumber -> MathExpression.newBuilder().apply { parseStartIndex = positiveRealNumber.startIndex parseEndIndex = positiveRealNumber.endIndex @@ -450,24 +486,23 @@ class NumericExpressionParser private constructor( is DivideSymbol, is EqualsSymbol, is ExponentiationSymbol, is FunctionName, is LeftParenthesisSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, null -> - MathParsingError.GenericError.toFailure() + GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() is InvalidToken -> nextToken.toFailure() } } private fun parseVariable(): MathParsingResult { val variableNameResult = - consumeTokenOfType().maybeFail { variableName -> - if (!parseContext.allowsVariable(variableName.parsedName)) { - MathParsingError.GenericError - } else null + parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.allowsVariables()) GenericError else null }.maybeFail { variableName -> - return@maybeFail if (tokens.hasNext()) { - when (val nextToken = tokens.peek()) { + return@maybeFail if (parseContext.hasMoreTokens()) { + when (val nextToken = parseContext.peekToken()) { is PositiveInteger -> - MathParsingError.NumberAfterVariableError(nextToken.toReal(), variableName.parsedName) + NumberAfterVariableError(nextToken.toReal(), variableName.parsedName) is PositiveRealNumber -> - MathParsingError.NumberAfterVariableError(nextToken.toReal(), variableName.parsedName) + NumberAfterVariableError(nextToken.toReal(), variableName.parsedName) else -> null } } else null @@ -488,20 +523,83 @@ class NumericExpressionParser private constructor( var lastLhsResult = parseLhs() while (!lastLhsResult.isFailure()) { // Compute the next LHS if there are further RHS expressions. - val (operator, rhsResult) = parseRhs(tokens.peek()) ?: break // Not a match to the expression. - lastLhsResult = lastLhsResult.combineWith(rhsResult) { lhs, rhs -> - MathExpression.newBuilder().apply { - parseStartIndex = lhs.parseStartIndex - parseEndIndex = rhs.parseEndIndex - binaryOperation = MathBinaryOperation.newBuilder().apply { - this.operator = operator - leftOperand = lhs - rightOperand = rhs - }.build() + lastLhsResult = + computeBinaryOperationExpression(lastLhsResult, parseRhs) + ?: break // Not a match to the expression. + } + return lastLhsResult + } + + private fun computeBinaryOperationExpression( + lhsResult: MathParsingResult, + parseRhs: (Token?) -> Pair>? + ): MathParsingResult? { + val (operator, rhsResult) = parseRhs(parseContext.peekToken()) ?: return null + return lhsResult.combineWith(rhsResult) { lhs, rhs -> + MathExpression.newBuilder().apply { + parseStartIndex = lhs.parseStartIndex + parseEndIndex = rhs.parseEndIndex + binaryOperation = MathBinaryOperation.newBuilder().apply { + this.operator = operator + leftOperand = lhs + rightOperand = rhs }.build() + }.build() + } + } + + private fun checkForLearnerErrors(expression: MathExpression): MathParsingError? { + val firstMultiRedundantGroup = expression.findFirstMultiRedundantGroup() + val nextRedundantGroup = expression.findNextRedundantGroup() + val nextUnaryOperation = expression.findNextRedundantUnaryOperation() + val nextExpWithVariableExp = expression.findNextExponentiationWithVariablePower() + val nextExpWithTooLargePower = expression.findNextExponentiationWithTooLargePower() + val nextExpWithNestedExp = expression.findNextNestedExponentiation() + val nextDivByZero = expression.findNextDivisionByZero() + val disallowedVariables = expression.findAllDisallowedVariables(parseContext) + // Note that the order of checks here is important since errors have precedence, and some are + // redundant and, in the wrong order, may cause the wrong error to be returned. + val includeOptionalErrors = parseContext.errorCheckingMode.includesOptionalErrors() + return when { + includeOptionalErrors && firstMultiRedundantGroup != null -> { + val subExpression = parseContext.extractSubexpression(firstMultiRedundantGroup) + MultipleRedundantParenthesesError(subExpression, firstMultiRedundantGroup) + } + includeOptionalErrors && expression.expressionTypeCase == GROUP -> + SingleRedundantParenthesesError(parseContext.rawExpression, expression) + includeOptionalErrors && nextRedundantGroup != null -> { + val subExpression = parseContext.extractSubexpression(nextRedundantGroup) + RedundantParenthesesForIndividualTermsError(subExpression, nextRedundantGroup) } + includeOptionalErrors && nextUnaryOperation != null -> SubsequentUnaryOperatorsError + includeOptionalErrors && nextExpWithVariableExp != null -> ExponentIsVariableExpressionError + includeOptionalErrors && nextExpWithTooLargePower != null -> ExponentTooLargeError + includeOptionalErrors && nextExpWithNestedExp != null -> NestedExponentsError + includeOptionalErrors && nextDivByZero != null -> TermDividedByZeroError + includeOptionalErrors && disallowedVariables.isNotEmpty() -> + DisabledVariablesInUseError(disallowedVariables.toList()) + else -> ensureNoRemainingTokens() } - return lastLhsResult + } + + private fun ensureNoRemainingTokens(): MathParsingError? { + // Make sure all tokens were consumed (otherwise there are trailing tokens which invalidate the + // whole grammar). + return if (parseContext.hasMoreTokens()) { + when (val nextToken = parseContext.peekToken()) { + is LeftParenthesisSymbol, is RightParenthesisSymbol -> UnbalancedParenthesesError + is EqualsSymbol -> { + if (parseContext is AlgebraicExpressionContext && parseContext.isPartOfEquation) { + EquationHasWrongNumberOfEqualsError + } else GenericError + } + is IncompleteFunctionName -> nextToken.toError() + is InvalidToken -> nextToken.toError() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is ExponentiationSymbol, + is FunctionName, is MinusSymbol, is MultiplySymbol, is PlusSymbol, is SquareRootSymbol, + is VariableName, null -> GenericError + } + } else null } private fun PositiveInteger.toReal(): Real = Real.newBuilder().apply { @@ -512,24 +610,49 @@ class NumericExpressionParser private constructor( irrational = parsedValue }.build() - private inline fun consumeTokenOfType( - missingError: () -> MathParsingError = { MathParsingError.GenericError } - ): MathParsingResult { - val maybeToken = tokens.expectNextMatches { it is T } as? T - return maybeToken?.let { token -> - MathParsingResult.Success(token) - } ?: missingError().toFailure() - } + @Suppress("unused") // The receiver is behaving as a namespace. + private fun IncompleteFunctionName.toError(): MathParsingError = FunctionNameIncompleteError private fun InvalidToken.toError(): MathParsingError = - MathParsingError.UnnecessarySymbolsError(parseContext.extractSubexpression(this)) + UnnecessarySymbolsError(parseContext.extractSubexpression(this)) + + private fun IncompleteFunctionName.toFailure(): MathParsingResult = toError().toFailure() private fun InvalidToken.toFailure(): MathParsingResult = toError().toFailure() private sealed class ParseContext(val rawExpression: String) { + val tokens: PeekableIterator by lazy { + PeekableIterator.fromSequence(MathTokenizer2.tokenize(rawExpression)) + } + private var previousToken: Token? = null + abstract val errorCheckingMode: ErrorCheckingMode - abstract fun allowsVariable(variableName: String): Boolean + abstract fun allowsVariables(): Boolean + + fun hasMoreTokens(): Boolean = tokens.hasNext() + + fun peekToken(): Token? = tokens.peek() + + /** + * Returns the last token consumed by [consumeTokenOfType], or null if none. Note: this should + * only be used for error reporting purposes, not for parsing. Using this for parsing would, in + * certain cases, allow for a non-LL(1) grammar which is against one design goal for this + * parser. + */ + fun getPreviousToken(): Token? = previousToken + + inline fun hasNextTokenOfType(): Boolean = peekToken() is T + + inline fun consumeTokenOfType( + missingError: () -> MathParsingError = { GenericError } + ): MathParsingResult { + val maybeToken = tokens.expectNextMatches { it is T } as? T + return maybeToken?.let { token -> + previousToken = token + MathParsingResult.Success(token) + } ?: missingError().toFailure() + } fun extractSubexpression(token: Token): String { return rawExpression.substring(token.startIndex, token.endIndex) @@ -543,15 +666,18 @@ class NumericExpressionParser private constructor( rawExpression: String, override val errorCheckingMode: ErrorCheckingMode ) : ParseContext(rawExpression) { // Numeric expressions never allow variables. - override fun allowsVariable(variableName: String): Boolean = false + override fun allowsVariables(): Boolean = false } class AlgebraicExpressionContext( rawExpression: String, + val isPartOfEquation: Boolean, private val allowedVariables: List, override val errorCheckingMode: ErrorCheckingMode ) : ParseContext(rawExpression) { - override fun allowsVariable(variableName: String): Boolean = variableName in allowedVariables + fun allowsVariable(variableName: String): Boolean = variableName in allowedVariables + + override fun allowsVariables(): Boolean = true } } @@ -578,7 +704,7 @@ class NumericExpressionParser private constructor( errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS ): MathParsingResult { return createAlgebraicParser( - rawExpression, allowedVariables, errorCheckingMode + rawExpression, isPartOfEquation = false, allowedVariables, errorCheckingMode ).parseGenericExpressionGrammar() } @@ -588,24 +714,25 @@ class NumericExpressionParser private constructor( errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS ): MathParsingResult { return createAlgebraicParser( - rawExpression, allowedVariables, errorCheckingMode + rawExpression, isPartOfEquation = true, allowedVariables, errorCheckingMode ).parseGenericEquationGrammar() } private fun createNumericParser( rawExpression: String, errorCheckingMode: ErrorCheckingMode - ): NumericExpressionParser { - return NumericExpressionParser( - rawExpression, NumericExpressionContext(rawExpression, errorCheckingMode) - ) - } + ): NumericExpressionParser = + NumericExpressionParser(NumericExpressionContext(rawExpression, errorCheckingMode)) private fun createAlgebraicParser( - rawExpression: String, allowedVariables: List, errorCheckingMode: ErrorCheckingMode + rawExpression: String, + isPartOfEquation: Boolean, + allowedVariables: List, + errorCheckingMode: ErrorCheckingMode ): NumericExpressionParser { return NumericExpressionParser( - rawExpression, - AlgebraicExpressionContext(rawExpression, allowedVariables, errorCheckingMode) + AlgebraicExpressionContext( + rawExpression, isPartOfEquation, allowedVariables, errorCheckingMode + ) ) } @@ -723,35 +850,166 @@ class NumericExpressionParser private constructor( } } } - } - private fun MathExpression.findFirstMultiRedundantGroup(): MathExpression? { - return when (expressionTypeCase) { - BINARY_OPERATION -> { - binaryOperation.leftOperand.findFirstMultiRedundantGroup() - ?: binaryOperation.rightOperand.findFirstMultiRedundantGroup() + private fun MathExpression.findFirstMultiRedundantGroup(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findFirstMultiRedundantGroup() + ?: binaryOperation.rightOperand.findFirstMultiRedundantGroup() + } + UNARY_OPERATION -> unaryOperation.operand.findFirstMultiRedundantGroup() + FUNCTION_CALL -> functionCall.argument.findFirstMultiRedundantGroup() + GROUP -> group.takeIf { it.expressionTypeCase == GROUP } + ?: group.findFirstMultiRedundantGroup() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextRedundantGroup(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findNextRedundantGroup() + ?: binaryOperation.rightOperand.findNextRedundantGroup() + } + UNARY_OPERATION -> unaryOperation.operand.findNextRedundantGroup() + FUNCTION_CALL -> functionCall.argument.findNextRedundantGroup() + GROUP -> group.takeIf { + it.expressionTypeCase in listOf(CONSTANT, VARIABLE) + } ?: group.findNextRedundantGroup() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextRedundantUnaryOperation(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findNextRedundantUnaryOperation() + ?: binaryOperation.rightOperand.findNextRedundantUnaryOperation() + } + UNARY_OPERATION -> unaryOperation.operand.takeIf { + it.expressionTypeCase == UNARY_OPERATION + } ?: unaryOperation.operand.findNextRedundantUnaryOperation() + FUNCTION_CALL -> functionCall.argument.findNextRedundantUnaryOperation() + GROUP -> group.findNextRedundantUnaryOperation() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextExponentiationWithVariablePower(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == EXPONENTIATE + && binaryOperation.rightOperand.isVariableExpression() + } ?: binaryOperation.leftOperand.findNextExponentiationWithVariablePower() + ?: binaryOperation.rightOperand.findNextExponentiationWithVariablePower() + } + UNARY_OPERATION -> unaryOperation.operand.findNextExponentiationWithVariablePower() + FUNCTION_CALL -> functionCall.argument.findNextExponentiationWithVariablePower() + GROUP -> group.findNextExponentiationWithVariablePower() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null } - UNARY_OPERATION -> unaryOperation.operand.findFirstMultiRedundantGroup() - FUNCTION_CALL -> functionCall.argument.findFirstMultiRedundantGroup() - GROUP -> group.takeIf { it.expressionTypeCase == GROUP } - ?: group.findFirstMultiRedundantGroup() - CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null } - } - private fun MathExpression.findNextRedundantGroup(): MathExpression? { - return when (expressionTypeCase) { - BINARY_OPERATION -> { - binaryOperation.leftOperand.findNextRedundantGroup() - ?: binaryOperation.rightOperand.findNextRedundantGroup() + private fun MathExpression.findNextExponentiationWithTooLargePower(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == EXPONENTIATE + && binaryOperation.rightOperand.expressionTypeCase == CONSTANT + && binaryOperation.rightOperand.constant.toDouble() > 5.0 + } ?: binaryOperation.leftOperand.findNextExponentiationWithTooLargePower() + ?: binaryOperation.rightOperand.findNextExponentiationWithTooLargePower() + } + UNARY_OPERATION -> unaryOperation.operand.findNextExponentiationWithTooLargePower() + FUNCTION_CALL -> functionCall.argument.findNextExponentiationWithTooLargePower() + GROUP -> group.findNextExponentiationWithTooLargePower() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null } - UNARY_OPERATION -> unaryOperation.operand.findNextRedundantGroup() - FUNCTION_CALL -> functionCall.argument.findNextRedundantGroup() - GROUP -> { - group.takeIf { it.expressionTypeCase in listOf(CONSTANT, VARIABLE) } - ?: group.findNextRedundantGroup() + } + + private fun MathExpression.findNextNestedExponentiation(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == EXPONENTIATE + && binaryOperation.rightOperand.containsExponentiation() + } ?: binaryOperation.leftOperand.findNextNestedExponentiation() + ?: binaryOperation.rightOperand.findNextNestedExponentiation() + } + UNARY_OPERATION -> unaryOperation.operand.findNextNestedExponentiation() + FUNCTION_CALL -> functionCall.argument.findNextNestedExponentiation() + GROUP -> group.findNextNestedExponentiation() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextDivisionByZero(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == DIVIDE + && binaryOperation.rightOperand.expressionTypeCase == CONSTANT + && binaryOperation.rightOperand.constant + .toDouble().absoluteValue.approximatelyEquals(0.0) + } ?: binaryOperation.leftOperand.findNextDivisionByZero() + ?: binaryOperation.rightOperand.findNextDivisionByZero() + } + UNARY_OPERATION -> unaryOperation.operand.findNextDivisionByZero() + FUNCTION_CALL -> functionCall.argument.findNextDivisionByZero() + GROUP -> group.findNextDivisionByZero() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findAllDisallowedVariables(context: ParseContext): Set { + return if (context is AlgebraicExpressionContext) { + findAllDisallowedVariablesAux(context) + } else setOf() + } + + private fun MathExpression.findAllDisallowedVariablesAux( + context: AlgebraicExpressionContext + ): Set { + return when (expressionTypeCase) { + VARIABLE -> if (context.allowsVariable(variable)) setOf() else setOf(variable) + BINARY_OPERATION -> { + binaryOperation.leftOperand.findAllDisallowedVariablesAux(context) + + binaryOperation.rightOperand.findAllDisallowedVariablesAux(context) + } + UNARY_OPERATION -> unaryOperation.operand.findAllDisallowedVariablesAux(context) + FUNCTION_CALL -> functionCall.argument.findAllDisallowedVariablesAux(context) + GROUP -> group.findAllDisallowedVariablesAux(context) + CONSTANT, EXPRESSIONTYPE_NOT_SET, null -> setOf() + } + } + + private fun MathExpression.isVariableExpression(): Boolean { + return when (expressionTypeCase) { + VARIABLE -> true + BINARY_OPERATION -> { + binaryOperation.leftOperand.isVariableExpression() + || binaryOperation.rightOperand.isVariableExpression() + } + UNARY_OPERATION -> unaryOperation.operand.isVariableExpression() + FUNCTION_CALL -> functionCall.argument.isVariableExpression() + GROUP -> group.isVariableExpression() + CONSTANT, EXPRESSIONTYPE_NOT_SET, null -> false + } + } + + private fun MathExpression.containsExponentiation(): Boolean { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.operator == EXPONENTIATE + || binaryOperation.leftOperand.containsExponentiation() + || binaryOperation.rightOperand.containsExponentiation() + } + UNARY_OPERATION -> unaryOperation.operand.containsExponentiation() + FUNCTION_CALL -> functionCall.argument.containsExponentiation() + GROUP -> group.containsExponentiation() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> false } - CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null } } } diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 402368c5f01..d1aca2346b7 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -28,12 +28,24 @@ import kotlin.math.sqrt import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError +import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError +import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError +import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError +import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError +import org.oppia.android.util.math.MathParsingError.FunctionNameIncompleteError import org.oppia.android.util.math.MathParsingError.HangingSquareRootError +import org.oppia.android.util.math.MathParsingError.InvalidFunctionInUseError import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.NestedExponentsError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberAfterBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberBeforeBinaryOperatorError import org.oppia.android.util.math.MathParsingError.NumberAfterVariableError import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError +import org.oppia.android.util.math.MathParsingError.SubsequentBinaryOperatorsError +import org.oppia.android.util.math.MathParsingError.SubsequentUnaryOperatorsError +import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError @@ -117,32 +129,166 @@ class NumericExpressionParserTest { assertThat((failure19 as NumberAfterVariableError).number.irrational).isWithin(1e-5).of(3.14) assertThat(failure19.variable).isEqualTo("y") - // SubsequentBinaryOperatorsError + // TODO: expand to multiple tests or use parametrized tests. + // RHS operators don't result in unary operations (which are valid in the grammar). + val rhsOperators = listOf("*", "×", "/", "÷", "^") + val lhsOperators = rhsOperators + listOf("+", "-", "−") + val operatorCombinations = lhsOperators.flatMap { op1 -> rhsOperators.map { op1 to it } } + for ((op1, op2) in operatorCombinations) { + val failure22 = expectFailureWhenParsingNumericExpression(expression = "1 $op1$op2 2") + assertThat(failure22).isInstanceOf(SubsequentBinaryOperatorsError::class.java) + assertThat((failure22 as SubsequentBinaryOperatorsError).operator1).isEqualTo(op1) + assertThat(failure22.operator2).isEqualTo(op2) + } + + val failure37 = expectFailureWhenParsingNumericExpression("++2") + assertThat(failure37).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure38 = expectFailureWhenParsingAlgebraicExpression("--x") + assertThat(failure38).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure39 = expectFailureWhenParsingAlgebraicExpression("-+x") + assertThat(failure39).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure40 = expectFailureWhenParsingNumericExpression("+-2") + assertThat(failure40).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + parseNumericExpressionWithAllErrors("2++3") // Will succeed since it's 2 + (+2). + val failure41 = expectFailureWhenParsingNumericExpression("2+++3") + assertThat(failure41).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure23 = expectFailureWhenParsingNumericExpression("/2") + assertThat(failure23).isInstanceOf(NoVariableOrNumberBeforeBinaryOperatorError::class.java) + assertThat((failure23 as NoVariableOrNumberBeforeBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.DIVIDE) + + val failure24 = expectFailureWhenParsingAlgebraicExpression("*x") + assertThat(failure24).isInstanceOf(NoVariableOrNumberBeforeBinaryOperatorError::class.java) + assertThat((failure24 as NoVariableOrNumberBeforeBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.MULTIPLY) + + val failure27 = expectFailureWhenParsingNumericExpression("2^") + assertThat(failure27).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure27 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.EXPONENTIATE) + + val failure25 = expectFailureWhenParsingNumericExpression("2/") + assertThat(failure25).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure25 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.DIVIDE) + + val failure26 = expectFailureWhenParsingAlgebraicExpression("x*") + assertThat(failure26).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure26 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.MULTIPLY) - // SubsequentUnaryOperatorsError -> analysis + val failure28 = expectFailureWhenParsingAlgebraicExpression("x+") + assertThat(failure28).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure28 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.ADD) - // NoVariableOrNumberBeforeBinaryOperatorError - // NoVariableOrNumberAfterBinaryOperatorError + val failure29 = expectFailureWhenParsingAlgebraicExpression("x-") + assertThat(failure29).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure29 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.SUBTRACT) - // ExponentIsVariableExpressionError -> analysis - // ExponentTooLargeError -> analysis - // NestedExponentsError -> analysis + val failure42 = expectFailureWhenParsingAlgebraicExpression("2^x") + assertThat(failure42).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure43 = expectFailureWhenParsingAlgebraicExpression("2^(1+x)") + assertThat(failure43).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure44 = expectFailureWhenParsingAlgebraicExpression("2^3^x") + assertThat(failure44).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure45 = expectFailureWhenParsingAlgebraicExpression("2^sqrt(x)") + assertThat(failure45).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure46 = expectFailureWhenParsingNumericExpression("2^7") + assertThat(failure46).isInstanceOf(ExponentTooLargeError::class.java) + + val failure47 = expectFailureWhenParsingNumericExpression("2^30.12") + assertThat(failure47).isInstanceOf(ExponentTooLargeError::class.java) + + parseNumericExpressionWithAllErrors("2^3") + + val failure48 = expectFailureWhenParsingNumericExpression("2^3^2") + assertThat(failure48).isInstanceOf(NestedExponentsError::class.java) + + val failure49 = expectFailureWhenParsingAlgebraicExpression("x^2^5") + assertThat(failure49).isInstanceOf(NestedExponentsError::class.java) val failure20 = expectFailureWhenParsingNumericExpression("2√") assertThat(failure20).isInstanceOf(HangingSquareRootError::class.java) - // TermDividedByZeroError -> analysis + val failure50 = expectFailureWhenParsingNumericExpression("2/0") + assertThat(failure50).isInstanceOf(TermDividedByZeroError::class.java) + + val failure51 = expectFailureWhenParsingAlgebraicExpression("x/0") + assertThat(failure51).isInstanceOf(TermDividedByZeroError::class.java) + + val failure52 = expectFailureWhenParsingNumericExpression("sqrt(2+7/0.0)") + assertThat(failure52).isInstanceOf(TermDividedByZeroError::class.java) val failure21 = expectFailureWhenParsingNumericExpression("x+y") assertThat(failure21).isInstanceOf(VariableInNumericExpressionError::class.java) - // DisabledVariablesInUseError -> analysis + val failure53 = expectFailureWhenParsingAlgebraicExpression("x+y+a") + assertThat(failure53).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure53 as DisabledVariablesInUseError).variables).containsExactly("a") - // EquationHasWrongNumberOfEqualsError - // EquationMissingLhsOrRhsError - // InvalidFunctionInUseError + val failure54 = expectFailureWhenParsingAlgebraicExpression("apple") + assertThat(failure54).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure54 as DisabledVariablesInUseError).variables) + .containsExactly("a", "p", "l", "e") - // FunctionNameUsedAsVariables -> analysis + val failure55 = + expectFailureWhenParsingAlgebraicExpression("apple", allowedVariables = listOf("a", "p", "l")) + assertThat(failure55).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure55 as DisabledVariablesInUseError).variables).containsExactly("e") + + parseAlgebraicExpressionWithAllErrors("x+y+z") + + val failure56 = + expectFailureWhenParsingAlgebraicExpression("x+y+z", allowedVariables = listOf()) + assertThat(failure56).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure56 as DisabledVariablesInUseError).variables).containsExactly("x", "y", "z") + + val failure30 = expectFailureWhenParsingAlgebraicEquation("x==2") + assertThat(failure30).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + + val failure31 = expectFailureWhenParsingAlgebraicEquation("x=2=y") + assertThat(failure31).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + + val failure32 = expectFailureWhenParsingAlgebraicEquation("x=2=") + assertThat(failure32).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + + val failure33 = expectFailureWhenParsingAlgebraicEquation("x=") + assertThat(failure33).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + + val failure34 = expectFailureWhenParsingAlgebraicEquation("=x") + assertThat(failure34).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + + val failure35 = expectFailureWhenParsingAlgebraicEquation("=x") + assertThat(failure35).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + + // TODO: expand to multiple tests or use parametrized tests. + val prohibitedFunctionNames = + listOf( + "exp", "log", "log10", "ln", "sin", "cos", "tan", "cot", "csc", "sec", "atan", "asin", + "acos", "abs" + ) + for (functionName in prohibitedFunctionNames) { + val failure36 = expectFailureWhenParsingAlgebraicEquation("$functionName(0.5)") + assertThat(failure36).isInstanceOf(InvalidFunctionInUseError::class.java) + assertThat((failure36 as InvalidFunctionInUseError).functionName).isEqualTo(functionName) + } + + val failure57 = expectFailureWhenParsingAlgebraicExpression("sq") + assertThat(failure57).isInstanceOf(FunctionNameIncompleteError::class.java) + + val failure58 = expectFailureWhenParsingAlgebraicExpression("sqr") + assertThat(failure58).isInstanceOf(FunctionNameIncompleteError::class.java) // TODO: Other cases: sqrt(, sqrt(), sqrt 2 } @@ -4324,8 +4470,12 @@ class NumericExpressionParserTest { return NumericExpressionParser.parseNumericExpression(expression, errorCheckingMode) } - private fun expectFailureWhenParsingAlgebraicExpression(expression: String): MathParsingError { - val result = parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) + private fun expectFailureWhenParsingAlgebraicExpression( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingError { + val result = + parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) return (result as MathParsingResult.Failure).error } @@ -4337,6 +4487,13 @@ class NumericExpressionParserTest { return (parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables) as MathParsingResult.Success).result } + private fun parseAlgebraicExpressionWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + return (parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) as MathParsingResult.Success).result + } + private fun parseAlgebraicExpressionInternal( expression: String, errorCheckingMode: ErrorCheckingMode, @@ -4347,9 +4504,10 @@ class NumericExpressionParserTest { ) } - private fun expectFailureWhenParsingAlgebraicEquation(expression: String) { - assertThat(parseAlgebraicEquationInternal(expression, ErrorCheckingMode.ALL_ERRORS)) - .isInstanceOf(MathParsingResult.Failure::class.java) + private fun expectFailureWhenParsingAlgebraicEquation(expression: String): MathParsingError { + val result = parseAlgebraicEquationInternal(expression, ErrorCheckingMode.ALL_ERRORS) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error } private fun parseAlgebraicEquationWithoutOptionalErrors( From 62edce190e4519a9eb1949d3061f01c2a4772ec1 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 6 Dec 2021 15:42:13 -0800 Subject: [PATCH 083/289] Add support for expression -> LaTeX conversion. --- .../util/math/MathExpressionExtensions.kt | 98 +++++++++++++++---- .../util/math/NumericExpressionParserTest.kt | 56 +++++++++-- 2 files changed, 126 insertions(+), 28 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index cd322b69b6e..3bc7655a0a2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -22,7 +22,11 @@ import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import kotlin.math.abs import kotlin.math.pow import kotlin.math.sqrt +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathFunctionCall.FunctionType +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator // TODO: split up this extensions file into multiple, clean it up, reorganize, and add tests. @@ -38,6 +42,56 @@ import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP // TODO: Make sure that all 'when' cases here do not use 'else' branches to ensure structural // changes require changing logic. +fun MathEquation.toRawLatex(divAsFraction: Boolean = false): String { + return "${leftSide.toRawLatex(divAsFraction)} = ${rightSide.toRawLatex(divAsFraction)}" +} + +fun MathExpression.toRawLatex(divAsFraction: Boolean = false): String = toRawLatexAux(divAsFraction) + +private fun MathExpression.toRawLatexAux(divAsFraction: Boolean): String { + return when (expressionTypeCase) { + CONSTANT -> when (constant.realTypeCase) { + RATIONAL -> constant.rational.toDouble().toPlainString() + IRRATIONAL -> constant.irrational.toPlainString() + INTEGER -> constant.integer.toString() + REALTYPE_NOT_SET, null -> "" + } + VARIABLE -> variable + BINARY_OPERATION -> { + val lhsLatex = binaryOperation.leftOperand.toRawLatexAux(divAsFraction) + val rhsLatex = binaryOperation.rightOperand.toRawLatexAux(divAsFraction) + when (binaryOperation.operator) { + BinaryOperator.ADD -> "$lhsLatex + $rhsLatex" + BinaryOperator.SUBTRACT -> "$lhsLatex - $rhsLatex" + BinaryOperator.MULTIPLY -> "$lhsLatex \\times $rhsLatex" + BinaryOperator.DIVIDE -> if (divAsFraction) { + "\\frac{$lhsLatex}{$rhsLatex}" + } else "$lhsLatex \\div $rhsLatex" + BinaryOperator.EXPONENTIATE -> "$lhsLatex ^ {$rhsLatex}" + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> + "$lhsLatex $rhsLatex" + } + } + UNARY_OPERATION -> { + val operandLatex = unaryOperation.operand.toRawLatexAux(divAsFraction) + when (unaryOperation.operator) { + UnaryOperator.NEGATE -> "-$operandLatex" + UnaryOperator.POSITIVE -> "+$operandLatex" + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> operandLatex + } + } + FUNCTION_CALL -> { + val argumentLatex = functionCall.argument.toRawLatexAux(divAsFraction) + when (functionCall.functionType) { + FunctionType.SQUARE_ROOT -> "\\sqrt{$argumentLatex}" + FunctionType.FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> argumentLatex + } + } + GROUP -> "(${group.toRawLatexAux(divAsFraction)})" + EXPRESSIONTYPE_NOT_SET, null -> "" + } +} + // TODO: add proper error channels for the return value. fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() @@ -55,37 +109,37 @@ fun MathExpression.evaluate(): Real? { private fun MathBinaryOperation.evaluate(): Real? { return when (operator) { - MathBinaryOperation.Operator.ADD -> + BinaryOperator.ADD -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.plus(it) } - MathBinaryOperation.Operator.SUBTRACT -> + BinaryOperator.SUBTRACT -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.minus(it) } - MathBinaryOperation.Operator.MULTIPLY -> + BinaryOperator.MULTIPLY -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.times(it) } - MathBinaryOperation.Operator.DIVIDE -> + BinaryOperator.DIVIDE -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.div(it) } - MathBinaryOperation.Operator.EXPONENTIATE -> + BinaryOperator.EXPONENTIATE -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.pow(it) } - MathBinaryOperation.Operator.OPERATOR_UNSPECIFIED, - MathBinaryOperation.Operator.UNRECOGNIZED, + BinaryOperator.OPERATOR_UNSPECIFIED, + BinaryOperator.UNRECOGNIZED, null -> null } } private fun MathUnaryOperation.evaluate(): Real? { return when (operator) { - MathUnaryOperation.Operator.NEGATE -> operand.evaluate()?.let { -it } - MathUnaryOperation.Operator.POSITIVE -> operand.evaluate() // '+2' is the same as just '2'. - MathUnaryOperation.Operator.OPERATOR_UNSPECIFIED, - MathUnaryOperation.Operator.UNRECOGNIZED, + UnaryOperator.NEGATE -> operand.evaluate()?.let { -it } + UnaryOperator.POSITIVE -> operand.evaluate() // '+2' is the same as just '2'. + UnaryOperator.OPERATOR_UNSPECIFIED, + UnaryOperator.UNRECOGNIZED, null -> null } } private fun MathFunctionCall.evaluate(): Real? { return when (functionType) { - MathFunctionCall.FunctionType.SQUARE_ROOT -> argument.evaluate()?.let { sqrt(it) } - MathFunctionCall.FunctionType.FUNCTION_UNSPECIFIED, - MathFunctionCall.FunctionType.UNRECOGNIZED, + FunctionType.SQUARE_ROOT -> argument.evaluate()?.let { sqrt(it) } + FunctionType.FUNCTION_UNSPECIFIED, + FunctionType.UNRECOGNIZED, null -> null } } @@ -192,7 +246,7 @@ private fun MathExpression.reduceToPolynomial(): Polynomial? { private fun MathUnaryOperation.reduceToPolynomial(): Polynomial? { return when (operator) { - MathUnaryOperation.Operator.NEGATE -> -(operand.reduceToPolynomial() ?: return null) + UnaryOperator.NEGATE -> -(operand.reduceToPolynomial() ?: return null) else -> null } } @@ -201,11 +255,11 @@ private fun MathBinaryOperation.reduceToPolynomial(): Polynomial? { val leftPolynomial = leftOperand.reduceToPolynomial() ?: return null val rightPolynomial = rightOperand.reduceToPolynomial() ?: return null return when (operator) { - MathBinaryOperation.Operator.ADD -> leftPolynomial + rightPolynomial - MathBinaryOperation.Operator.SUBTRACT -> leftPolynomial - rightPolynomial - MathBinaryOperation.Operator.MULTIPLY -> leftPolynomial * rightPolynomial - MathBinaryOperation.Operator.DIVIDE -> leftPolynomial / rightPolynomial - MathBinaryOperation.Operator.EXPONENTIATE -> leftPolynomial.pow(rightPolynomial) + BinaryOperator.ADD -> leftPolynomial + rightPolynomial + BinaryOperator.SUBTRACT -> leftPolynomial - rightPolynomial + BinaryOperator.MULTIPLY -> leftPolynomial * rightPolynomial + BinaryOperator.DIVIDE -> leftPolynomial / rightPolynomial + BinaryOperator.EXPONENTIATE -> leftPolynomial.pow(rightPolynomial) else -> null } } @@ -358,7 +412,7 @@ private fun Polynomial.getLeadingTerm(): Term { private fun Polynomial.getDegree(): Int = getLeadingTerm().highestDegree() private fun Term.highestDegree(): Int { - return variableList.map(Variable::getPower).max() ?: 0 + return variableList.map(Variable::getPower).maxOrNull() ?: 0 } private fun Polynomial.isApproximatelyZero(): Boolean { @@ -778,3 +832,5 @@ private fun sqrt(int: Int): Real { irrational = sqrt(int.toDouble()) }.build() } + +private fun Double.toPlainString(): String = toBigDecimal().toPlainString() diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index d1aca2346b7..0cccdc02ff1 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -4037,7 +4037,7 @@ class NumericExpressionParserTest { expectFailureWhenParsingAlgebraicEquation(" x =") expectFailureWhenParsingAlgebraicEquation(" = y") - val equation1 = parseAlgebraicEquationWithoutOptionalErrors("x = 1") + val equation1 = parseAlgebraicEquationWithAllErrors("x = 1") assertThat(equation1).hasLeftHandSideThat().hasStructureThatMatches { variable { withNameThat().isEqualTo("x") @@ -4046,7 +4046,7 @@ class NumericExpressionParserTest { assertThat(equation1).hasRightHandSideThat().evaluatesToIntegerThat().isEqualTo(1) val equation2 = - parseAlgebraicEquationWithoutOptionalErrors("y = mx + b", allowedVariables = listOf("x", "y", "b", "m")) + parseAlgebraicEquationWithAllErrors("y = mx + b", allowedVariables = listOf("x", "y", "b", "m")) assertThat(equation2).hasLeftHandSideThat().hasStructureThatMatches { variable { withNameThat().isEqualTo("y") @@ -4076,7 +4076,7 @@ class NumericExpressionParserTest { } } - val equation3 = parseAlgebraicEquationWithoutOptionalErrors("y = (x+1)^2") + val equation3 = parseAlgebraicEquationWithAllErrors("y = (x+1)^2") assertThat(equation3).hasLeftHandSideThat().hasStructureThatMatches { variable { withNameThat().isEqualTo("y") @@ -4108,7 +4108,7 @@ class NumericExpressionParserTest { } } - val equation4 = parseAlgebraicEquationWithoutOptionalErrors("y = (x+1)(x-1)") + val equation4 = parseAlgebraicEquationWithAllErrors("y = (x+1)(x-1)") assertThat(equation4).hasLeftHandSideThat().hasStructureThatMatches { variable { withNameThat().isEqualTo("y") @@ -4155,7 +4155,7 @@ class NumericExpressionParserTest { expectFailureWhenParsingAlgebraicEquation("y 2 = (x+1)(x-1)") val equation5 = - parseAlgebraicEquationWithoutOptionalErrors("a*x^2 + b*x + c = 0", allowedVariables = listOf("x", "a", "b", "c")) + parseAlgebraicEquationWithAllErrors("a*x^2 + b*x + c = 0", allowedVariables = listOf("x", "a", "b", "c")) assertThat(equation5).hasLeftHandSideThat().hasStructureThatMatches { addition { leftOperand { @@ -4213,6 +4213,48 @@ class NumericExpressionParserTest { } } + @Test + fun testLatex() { + // TODO: split up & move to separate test suites. Finish test cases. + + val exp1 = parseNumericExpressionWithAllErrors("1") + assertThat(exp1.toRawLatex()).isEqualTo("1") + + val exp2 = parseNumericExpressionWithAllErrors("1+2") + assertThat(exp2.toRawLatex()).isEqualTo("1 + 2") + + val exp3 = parseNumericExpressionWithAllErrors("1*2") + assertThat(exp3.toRawLatex()).isEqualTo("1 \\times 2") + + val exp4 = parseNumericExpressionWithAllErrors("1/2") + assertThat(exp4.toRawLatex()).isEqualTo("1 \\div 2") + + val exp5 = parseNumericExpressionWithAllErrors("1/2") + assertThat(exp5.toRawLatex(divAsFraction = true)).isEqualTo("\\frac{1}{2}") + + val exp6 = parseAlgebraicExpressionWithAllErrors("x+y") + assertThat(exp6.toRawLatex()).isEqualTo("x + y") + + val exp7 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1/y)") + assertThat(exp7.toRawLatex()).isEqualTo("x ^ {(1 \\div y)}") + + val exp8 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1/y)") + assertThat(exp8.toRawLatex(divAsFraction = true)).isEqualTo("x ^ {(\\frac{1}{y})}") + + val exp9 = parseAlgebraicExpressionWithoutOptionalErrors("x^y^z") + assertThat(exp9.toRawLatex(divAsFraction = true)).isEqualTo("x ^ {y ^ {z}}") + + val eq1 = + parseAlgebraicEquationWithAllErrors("a^2+b^2+c^2=0", allowedVariables = listOf("a", "b", "c")) + assertThat(eq1.toRawLatex()).isEqualTo("a ^ {2} + b ^ {2} + c ^ {2} = 0") + + val eq2 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") + assertThat(eq2.toRawLatex()).isEqualTo("\\sqrt{1 + x} \\div x = 1") + + val eq3 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") + assertThat(eq3.toRawLatex(divAsFraction = true)).isEqualTo("\\frac{\\sqrt{1 + x}}{x} = 1") + } + @DslMarker private annotation class ExpressionComparatorMarker @@ -4510,11 +4552,11 @@ class NumericExpressionParserTest { return (result as MathParsingResult.Failure).error } - private fun parseAlgebraicEquationWithoutOptionalErrors( + private fun parseAlgebraicEquationWithAllErrors( expression: String, allowedVariables: List = listOf("x", "y", "z") ): MathEquation { - return (parseAlgebraicEquationInternal(expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables) as MathParsingResult.Success).result + return (parseAlgebraicEquationInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) as MathParsingResult.Success).result } private fun parseAlgebraicEquationInternal( From 948d9f636a221952f93a1b536ec992a4b7a7e190 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 7 Dec 2021 14:18:20 -0800 Subject: [PATCH 084/289] Add support for generating a11y strings from math. This also adds some more tests for LaTeX cases, and does some other minor cleaning up. --- model/src/main/proto/math.proto | 1 + .../org/oppia/android/util/math/BUILD.bazel | 1 + .../util/math/MathExpressionExtensions.kt | 188 ++++++++- .../util/math/NumericExpressionParser.kt | 91 +++-- .../util/math/NumericExpressionParserTest.kt | 356 +++++++++++++++++- 5 files changed, 591 insertions(+), 46 deletions(-) diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index a8b37c3e877..c0ddd516723 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -54,6 +54,7 @@ message MathBinaryOperation { Operator operator = 1; MathExpression left_operand = 2; MathExpression right_operand = 3; + bool is_implicit = 4; } message MathUnaryOperation { diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index f447a817474..56f1470dbae 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -15,6 +15,7 @@ kt_android_library( "//:oppia_api_visibility", ], deps = [ + "//model/src/main/proto:languages_java_proto_lite", "//model/src/main/proto:math_java_proto_lite", ], ) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 3bc7655a0a2..e05f81a5fd8 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -1,5 +1,7 @@ package org.oppia.android.util.math +import java.text.NumberFormat +import java.util.Locale import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.MathBinaryOperation import org.oppia.android.app.model.MathExpression @@ -27,6 +29,15 @@ import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP import org.oppia.android.app.model.MathFunctionCall.FunctionType import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator +import org.oppia.android.app.model.OppiaLanguage +import org.oppia.android.app.model.OppiaLanguage.ARABIC +import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.ENGLISH +import org.oppia.android.app.model.OppiaLanguage.HINDI +import org.oppia.android.app.model.OppiaLanguage.HINGLISH +import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED +import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED // TODO: split up this extensions file into multiple, clean it up, reorganize, and add tests. @@ -42,6 +53,158 @@ import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator // TODO: Make sure that all 'when' cases here do not use 'else' branches to ensure structural // changes require changing logic. +// TODO: move these to the UI layer & have them utilize non-translatable strings. +private val numberFormat by lazy { NumberFormat.getNumberInstance(Locale.US) } +private val singularOrdinalNames = mapOf( + 1 to "oneth", + 2 to "half", + 3 to "third", + 4 to "fourth", + 5 to "fifth", + 6 to "sixth", + 7 to "seventh", + 8 to "eighth", + 9 to "ninth", + 10 to "tenth", +) +private val pluralOrdinalNames = mapOf( + 1 to "oneths", + 2 to "halves", + 3 to "thirds", + 4 to "fourths", + 5 to "fifths", + 6 to "sixths", + 7 to "sevenths", + 8 to "eighths", + 9 to "ninths", + 10 to "tenths", +) + +fun MathEquation.toHumanReadableString( + language: OppiaLanguage, + divAsFraction: Boolean = false +): String? { + return when (language) { + ENGLISH -> toHumanReadableEnglishString(divAsFraction) + ARABIC, HINDI, HINGLISH, PORTUGUESE, BRAZILIAN_PORTUGUESE, LANGUAGE_UNSPECIFIED, UNRECOGNIZED -> + null + } +} + +fun MathExpression.toHumanReadableString( + language: OppiaLanguage, + divAsFraction: Boolean = false +): String? { + return when (language) { + ENGLISH -> toHumanReadableEnglishString(divAsFraction) + ARABIC, HINDI, HINGLISH, PORTUGUESE, BRAZILIAN_PORTUGUESE, LANGUAGE_UNSPECIFIED, UNRECOGNIZED -> + null + } +} + +private fun MathEquation.toHumanReadableEnglishString(divAsFraction: Boolean): String? { + val lhsStr = leftSide.toHumanReadableEnglishString(divAsFraction) + val rhsStr = rightSide.toHumanReadableEnglishString(divAsFraction) + return if (lhsStr != null && rhsStr != null) "$lhsStr equals $rhsStr" else null +} + +private fun MathExpression.toHumanReadableEnglishString(divAsFraction: Boolean): String? { + // Reference: + // https://docs.google.com/document/d/1P-dldXQ08O-02ZRG978paiWOSz0dsvcKpDgiV_rKH_Y/view. + return when (expressionTypeCase) { + CONSTANT -> if (constant.realTypeCase == INTEGER) { + numberFormat.format(constant.integer.toLong()) + } else constant.toPlainString() + VARIABLE -> when (variable) { + "z" -> "zed" + "Z" -> "Zed" + else -> variable + } + BINARY_OPERATION -> { + val lhs = binaryOperation.leftOperand + val rhs = binaryOperation.rightOperand + val lhsStr = lhs.toHumanReadableEnglishString(divAsFraction) + val rhsStr = rhs.toHumanReadableEnglishString(divAsFraction) + if (lhsStr == null || rhsStr == null) return null + when (binaryOperation.operator) { + BinaryOperator.ADD -> "$lhsStr plus $rhsStr" + BinaryOperator.SUBTRACT -> "$lhsStr minus $rhsStr" + BinaryOperator.MULTIPLY -> { + if (binaryOperation.canBeReadAsImplicitMultiplication()) { + "$lhsStr $rhsStr" + } else "$lhsStr times $rhsStr" + } + BinaryOperator.DIVIDE -> { + if (divAsFraction && lhs.isConstantInteger() && rhs.isConstantInteger()) { + val numerator = lhs.constant.integer + val denominator = rhs.constant.integer + if (numerator in 0..10 && denominator in 1..10 && denominator >= numerator) { + val ordinalName = + if (numerator == 1) { + singularOrdinalNames.getValue(denominator) + } else pluralOrdinalNames.getValue(denominator) + "$numerator $ordinalName" + } else "$lhsStr over $rhsStr" + } else if (divAsFraction) { + "the fraction with numerator $lhsStr and denominator $rhsStr" + } else "$lhsStr divided by $rhsStr" + } + BinaryOperator.EXPONENTIATE -> "$lhsStr raised to the power of $rhsStr" + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null + } + } + UNARY_OPERATION -> { + val operandStr = unaryOperation.operand.toHumanReadableEnglishString(divAsFraction) + when (unaryOperation.operator) { + UnaryOperator.NEGATE -> operandStr?.let { "negative $it" } + UnaryOperator.POSITIVE -> operandStr?.let { "positive $it" } + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> null + } + } + FUNCTION_CALL -> { + val argStr = functionCall.argument.toHumanReadableEnglishString(divAsFraction) + when (functionCall.functionType) { + FunctionType.SQUARE_ROOT -> argStr?.let { + if (functionCall.argument.isSingleTerm()) { + "square root of $it" + } else "start square root $it end square root" + } + FunctionType.FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> null + } + } + GROUP -> group.toHumanReadableEnglishString(divAsFraction)?.let { + if (isSingleTerm()) it else "open parenthesis $it close parenthesis" + } + EXPRESSIONTYPE_NOT_SET, null -> null + } +} + +private fun MathBinaryOperation.canBeReadAsImplicitMultiplication(): Boolean { + // Note that exponentiation is specialized since it's higher precedence than multiplication which + // means the graph won't look like "constant * variable" for polynomial terms like 2x^4 (which are + // cases the system should read using implicit multiplication, e.g. "two x raised to the power of + // 4"). + if (!isImplicit || !leftOperand.isConstant()) return false + return rightOperand.isVariable() || rightOperand.isExponentiation() +} + +private fun MathExpression.isConstantInteger(): Boolean = + expressionTypeCase == CONSTANT && constant.realTypeCase == INTEGER + +private fun MathExpression.isConstant(): Boolean = expressionTypeCase == CONSTANT + +private fun MathExpression.isVariable(): Boolean = expressionTypeCase == VARIABLE + +private fun MathExpression.isExponentiation(): Boolean = + expressionTypeCase == BINARY_OPERATION && binaryOperation.operator == BinaryOperator.EXPONENTIATE + +private fun MathExpression.isSingleTerm(): Boolean = when (expressionTypeCase) { + CONSTANT, VARIABLE, FUNCTION_CALL -> true + BINARY_OPERATION, UNARY_OPERATION -> false + GROUP -> group.isSingleTerm() + EXPRESSIONTYPE_NOT_SET, null -> false +} + fun MathEquation.toRawLatex(divAsFraction: Boolean = false): String { return "${leftSide.toRawLatex(divAsFraction)} = ${rightSide.toRawLatex(divAsFraction)}" } @@ -50,12 +213,7 @@ fun MathExpression.toRawLatex(divAsFraction: Boolean = false): String = toRawLat private fun MathExpression.toRawLatexAux(divAsFraction: Boolean): String { return when (expressionTypeCase) { - CONSTANT -> when (constant.realTypeCase) { - RATIONAL -> constant.rational.toDouble().toPlainString() - IRRATIONAL -> constant.irrational.toPlainString() - INTEGER -> constant.integer.toString() - REALTYPE_NOT_SET, null -> "" - } + CONSTANT -> constant.toPlainString() VARIABLE -> variable BINARY_OPERATION -> { val lhsLatex = binaryOperation.leftOperand.toRawLatexAux(divAsFraction) @@ -63,7 +221,9 @@ private fun MathExpression.toRawLatexAux(divAsFraction: Boolean): String { when (binaryOperation.operator) { BinaryOperator.ADD -> "$lhsLatex + $rhsLatex" BinaryOperator.SUBTRACT -> "$lhsLatex - $rhsLatex" - BinaryOperator.MULTIPLY -> "$lhsLatex \\times $rhsLatex" + BinaryOperator.MULTIPLY -> if (binaryOperation.isImplicit) { + "$lhsLatex$rhsLatex" + } else "$lhsLatex \\times $rhsLatex" BinaryOperator.DIVIDE -> if (divAsFraction) { "\\frac{$lhsLatex}{$rhsLatex}" } else "$lhsLatex \\div $rhsLatex" @@ -228,10 +388,20 @@ private fun Variable.toAnswerString(): String { return if (power > 1) "$name^$power" else name } -private fun Real.toAnswerString(): String { +private fun Real.toAnswerString(): String = when (realTypeCase) { // Note that the rational part is first converted to an improper fraction since mixed fractions // can't be expressed as a single coefficient in typical polynomial syntax). - return if (hasRational()) rational.toImproperForm().toAnswerString() else irrational.toString() + RATIONAL -> rational.toImproperForm().toAnswerString() + IRRATIONAL -> irrational.toPlainString() + INTEGER -> integer.toString() + REALTYPE_NOT_SET, null -> "" +} + +private fun Real.toPlainString(): String = when (realTypeCase) { + RATIONAL -> rational.toDouble().toPlainString() + IRRATIONAL -> irrational.toPlainString() + INTEGER -> integer.toString() + REALTYPE_NOT_SET, null -> "" } private fun MathExpression.reduceToPolynomial(): Polynomial? { diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt index 067687da5f5..8423f072745 100644 --- a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt @@ -127,8 +127,14 @@ class NumericExpressionParser private constructor(private val parseContext: Pars // generic_add_sub_expression_rhs = // generic_add_expression_rhs | generic_sub_expression_rhs ; when (nextToken) { - is PlusSymbol -> ADD to parseGenericAddExpressionRhs() - is MinusSymbol -> SUBTRACT to parseGenericSubExpressionRhs() + is PlusSymbol -> BinaryOperationRhs( + operator = ADD, + rhsResult = parseGenericAddExpressionRhs() + ) + is MinusSymbol -> BinaryOperationRhs( + operator = SUBTRACT, + rhsResult = parseGenericSubExpressionRhs() + ) is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, is ExponentiationSymbol, is FunctionName, is InvalidToken, is LeftParenthesisSymbol, is MultiplySymbol, is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, @@ -170,13 +176,26 @@ class NumericExpressionParser private constructor(private val parseContext: Pars // | generic_div_expression_rhs // | generic_implicit_mult_expression_rhs ; when (nextToken) { - is MultiplySymbol -> MULTIPLY to parseGenericMultExpressionRhs() - is DivideSymbol -> DIVIDE to parseGenericDivExpressionRhs() - is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> - MULTIPLY to parseGenericImplicitMultExpressionRhs() + is MultiplySymbol -> BinaryOperationRhs( + operator = MULTIPLY, + rhsResult = parseGenericMultExpressionRhs() + ) + is DivideSymbol -> BinaryOperationRhs( + operator = DIVIDE, + rhsResult = parseGenericDivExpressionRhs() + ) + is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> BinaryOperationRhs( + operator = MULTIPLY, + rhsResult = parseGenericImplicitMultExpressionRhs(), + isImplicit = true + ) is VariableName -> { if (parseContext is AlgebraicExpressionContext) { - MULTIPLY to parseGenericImplicitMultExpressionRhs() + BinaryOperationRhs( + operator = MULTIPLY, + rhsResult = parseGenericImplicitMultExpressionRhs(), + isImplicit = true + ) } else null } // Not a match to the expression. @@ -245,11 +264,10 @@ class NumericExpressionParser private constructor(private val parseContext: Pars private fun parseGenericExpExpressionTail( lhsResult: MathParsingResult ): MathParsingResult { - return computeBinaryOperationExpression( - lhsResult - ) { - // generic_exp_expression_tail = exponentiation_operator , generic_exp_expression ; - EXPONENTIATE to lhsResult.flatMap { + // generic_exp_expression_tail = exponentiation_operator , generic_exp_expression ; + return BinaryOperationRhs( + operator = EXPONENTIATE, + rhsResult = lhsResult.flatMap { parseContext.consumeTokenOfType() }.maybeFail { if (!parseContext.hasMoreTokens()) { @@ -258,7 +276,7 @@ class NumericExpressionParser private constructor(private val parseContext: Pars }.flatMap { parseGenericExpExpression() } - } ?: GenericError.toFailure() + ).computeBinaryOperationExpression(lhsResult) } private fun parseGenericTermWithUnary(): MathParsingResult { @@ -517,37 +535,19 @@ class NumericExpressionParser private constructor(private val parseContext: Pars } private fun parseGenericBinaryExpression( - parseLhs: () -> MathParsingResult, - parseRhs: (Token?) -> Pair>? + parseLhs: () -> MathParsingResult, parseRhs: (Token?) -> BinaryOperationRhs? ): MathParsingResult { var lastLhsResult = parseLhs() while (!lastLhsResult.isFailure()) { // Compute the next LHS if there are further RHS expressions. lastLhsResult = - computeBinaryOperationExpression(lastLhsResult, parseRhs) + parseRhs(parseContext.peekToken()) + ?.computeBinaryOperationExpression(lastLhsResult) ?: break // Not a match to the expression. } return lastLhsResult } - private fun computeBinaryOperationExpression( - lhsResult: MathParsingResult, - parseRhs: (Token?) -> Pair>? - ): MathParsingResult? { - val (operator, rhsResult) = parseRhs(parseContext.peekToken()) ?: return null - return lhsResult.combineWith(rhsResult) { lhs, rhs -> - MathExpression.newBuilder().apply { - parseStartIndex = lhs.parseStartIndex - parseEndIndex = rhs.parseEndIndex - binaryOperation = MathBinaryOperation.newBuilder().apply { - this.operator = operator - leftOperand = lhs - rightOperand = rhs - }.build() - }.build() - } - } - private fun checkForLearnerErrors(expression: MathExpression): MathParsingError? { val firstMultiRedundantGroup = expression.findFirstMultiRedundantGroup() val nextRedundantGroup = expression.findNextRedundantGroup() @@ -851,6 +851,29 @@ class NumericExpressionParser private constructor(private val parseContext: Pars } } + private data class BinaryOperationRhs( + val operator: MathBinaryOperation.Operator, + val rhsResult: MathParsingResult, + val isImplicit: Boolean = false + ) { + fun computeBinaryOperationExpression( + lhsResult: MathParsingResult + ): MathParsingResult { + return lhsResult.combineWith(rhsResult) { lhs, rhs -> + MathExpression.newBuilder().apply { + parseStartIndex = lhs.parseStartIndex + parseEndIndex = rhs.parseEndIndex + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = this@BinaryOperationRhs.operator + leftOperand = lhs + rightOperand = rhs + isImplicit = this@BinaryOperationRhs.isImplicit + }.build() + }.build() + } + } + } + private fun MathExpression.findFirstMultiRedundantGroup(): MathExpression? { return when (expressionTypeCase) { BINARY_OPERATION -> { diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 0cccdc02ff1..f89c5f5ddbd 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -27,6 +27,14 @@ import org.robolectric.annotation.LooperMode import kotlin.math.sqrt import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.OppiaLanguage.ARABIC +import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.ENGLISH +import org.oppia.android.app.model.OppiaLanguage.HINDI +import org.oppia.android.app.model.OppiaLanguage.HINGLISH +import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED +import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError @@ -290,7 +298,7 @@ class NumericExpressionParserTest { val failure58 = expectFailureWhenParsingAlgebraicExpression("sqr") assertThat(failure58).isInstanceOf(FunctionNameIncompleteError::class.java) - // TODO: Other cases: sqrt(, sqrt(), sqrt 2 + // TODO: Other cases: sqrt(, sqrt(), sqrt 2, +2 } @Test @@ -4232,6 +4240,12 @@ class NumericExpressionParserTest { val exp5 = parseNumericExpressionWithAllErrors("1/2") assertThat(exp5.toRawLatex(divAsFraction = true)).isEqualTo("\\frac{1}{2}") + val exp10 = parseNumericExpressionWithAllErrors("√2") + assertThat(exp10.toRawLatex()).isEqualTo("\\sqrt{2}") + + val exp11 = parseNumericExpressionWithAllErrors("√(1/2)") + assertThat(exp11.toRawLatex()).isEqualTo("\\sqrt{(1 \\div 2)}") + val exp6 = parseAlgebraicExpressionWithAllErrors("x+y") assertThat(exp6.toRawLatex()).isEqualTo("x + y") @@ -4245,8 +4259,10 @@ class NumericExpressionParserTest { assertThat(exp9.toRawLatex(divAsFraction = true)).isEqualTo("x ^ {y ^ {z}}") val eq1 = - parseAlgebraicEquationWithAllErrors("a^2+b^2+c^2=0", allowedVariables = listOf("a", "b", "c")) - assertThat(eq1.toRawLatex()).isEqualTo("a ^ {2} + b ^ {2} + c ^ {2} = 0") + parseAlgebraicEquationWithAllErrors( + "7a^2+b^2+c^2=0", allowedVariables = listOf("a", "b", "c") + ) + assertThat(eq1.toRawLatex()).isEqualTo("7a ^ {2} + b ^ {2} + c ^ {2} = 0") val eq2 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") assertThat(eq2.toRawLatex()).isEqualTo("\\sqrt{1 + x} \\div x = 1") @@ -4255,6 +4271,340 @@ class NumericExpressionParserTest { assertThat(eq3.toRawLatex(divAsFraction = true)).isEqualTo("\\frac{\\sqrt{1 + x}}{x} = 1") } + @Test + fun testHumanReadableString() { + // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). + + val exp1 = parseNumericExpressionWithAllErrors("1") + assertThat(exp1.toHumanReadableString(ARABIC)).isNull() + + assertThat(exp1.toHumanReadableString(HINDI)).isNull() + + assertThat(exp1.toHumanReadableString(HINGLISH)).isNull() + + assertThat(exp1.toHumanReadableString(PORTUGUESE)).isNull() + + assertThat(exp1.toHumanReadableString(BRAZILIAN_PORTUGUESE)).isNull() + + assertThat(exp1.toHumanReadableString(LANGUAGE_UNSPECIFIED)).isNull() + + assertThat(exp1.toHumanReadableString(UNRECOGNIZED)).isNull() + + val exp2 = parseAlgebraicExpressionWithAllErrors("x") + assertThat(exp2.toHumanReadableString(ARABIC)).isNull() + + assertThat(exp2.toHumanReadableString(HINDI)).isNull() + + assertThat(exp2.toHumanReadableString(HINGLISH)).isNull() + + assertThat(exp2.toHumanReadableString(PORTUGUESE)).isNull() + + assertThat(exp2.toHumanReadableString(BRAZILIAN_PORTUGUESE)).isNull() + + assertThat(exp2.toHumanReadableString(LANGUAGE_UNSPECIFIED)).isNull() + + assertThat(exp2.toHumanReadableString(UNRECOGNIZED)).isNull() + + val eq1 = parseAlgebraicEquationWithAllErrors("x=1") + assertThat(eq1.toHumanReadableString(ARABIC)).isNull() + + assertThat(eq1.toHumanReadableString(HINDI)).isNull() + + assertThat(eq1.toHumanReadableString(HINGLISH)).isNull() + + assertThat(eq1.toHumanReadableString(PORTUGUESE)).isNull() + + assertThat(eq1.toHumanReadableString(BRAZILIAN_PORTUGUESE)).isNull() + + assertThat(eq1.toHumanReadableString(LANGUAGE_UNSPECIFIED)).isNull() + + assertThat(eq1.toHumanReadableString(UNRECOGNIZED)).isNull() + + // specific cases (from rules & other cases): + val exp3 = parseNumericExpressionWithAllErrors("1") + assertThat(exp3.toHumanReadableString(ENGLISH)).isEqualTo("1") + + val exp49 = parseNumericExpressionWithAllErrors("-1") + assertThat(exp49.toHumanReadableString(ENGLISH)).isEqualTo("negative 1") + + val exp50 = parseNumericExpressionWithAllErrors("+1") + assertThat(exp50.toHumanReadableString(ENGLISH)).isEqualTo("positive 1") + + val exp4 = parseNumericExpressionWithoutOptionalErrors("((1))") + assertThat(exp4.toHumanReadableString(ENGLISH)).isEqualTo("1") + + val exp5 = parseNumericExpressionWithAllErrors("1+2") + assertThat(exp5.toHumanReadableString(ENGLISH)).isEqualTo("1 plus 2") + + val exp6 = parseNumericExpressionWithAllErrors("1-2") + assertThat(exp6.toHumanReadableString(ENGLISH)).isEqualTo("1 minus 2") + + val exp7 = parseNumericExpressionWithAllErrors("1*2") + assertThat(exp7.toHumanReadableString(ENGLISH)).isEqualTo("1 times 2") + + val exp8 = parseNumericExpressionWithAllErrors("1/2") + assertThat(exp8.toHumanReadableString(ENGLISH)).isEqualTo("1 divided by 2") + + val exp9 = parseNumericExpressionWithAllErrors("1+(1-2)") + assertThat(exp9.toHumanReadableString(ENGLISH)) + .isEqualTo("1 plus open parenthesis 1 minus 2 close parenthesis") + + val exp10 = parseNumericExpressionWithAllErrors("2^3") + assertThat(exp10.toHumanReadableString(ENGLISH)).isEqualTo("2 raised to the power of 3") + + val exp11 = parseNumericExpressionWithAllErrors("2^(1+2)") + assertThat(exp11.toHumanReadableString(ENGLISH)) + .isEqualTo("2 raised to the power of open parenthesis 1 plus 2 close parenthesis") + + val exp12 = parseNumericExpressionWithAllErrors("100000*2") + assertThat(exp12.toHumanReadableString(ENGLISH)).isEqualTo("100,000 times 2") + + val exp13 = parseNumericExpressionWithAllErrors("sqrt(2)") + assertThat(exp13.toHumanReadableString(ENGLISH)).isEqualTo("square root of 2") + + val exp14 = parseNumericExpressionWithAllErrors("√2") + assertThat(exp14.toHumanReadableString(ENGLISH)).isEqualTo("square root of 2") + + val exp15 = parseNumericExpressionWithAllErrors("sqrt(1+2)") + assertThat(exp15.toHumanReadableString(ENGLISH)) + .isEqualTo("start square root 1 plus 2 end square root") + + val singularOrdinalNames = mapOf( + 1 to "oneth", + 2 to "half", + 3 to "third", + 4 to "fourth", + 5 to "fifth", + 6 to "sixth", + 7 to "seventh", + 8 to "eighth", + 9 to "ninth", + 10 to "tenth", + ) + val pluralOrdinalNames = mapOf( + 1 to "oneths", + 2 to "halves", + 3 to "thirds", + 4 to "fourths", + 5 to "fifths", + 6 to "sixths", + 7 to "sevenths", + 8 to "eighths", + 9 to "ninths", + 10 to "tenths", + ) + for (denominatorToCheck in 1..10) { + for (numeratorToCheck in 0..denominatorToCheck) { + val exp16 = parseNumericExpressionWithAllErrors("$numeratorToCheck/$denominatorToCheck") + + val ordinalName = + if (numeratorToCheck == 1) { + singularOrdinalNames.getValue(denominatorToCheck) + } else pluralOrdinalNames.getValue(denominatorToCheck) + assertThat(exp16.toHumanReadableString(ENGLISH, divAsFraction = true)) + .isEqualTo("$numeratorToCheck $ordinalName") + } + } + + val exp17 = parseNumericExpressionWithAllErrors("-1/3") + assertThat(exp17.toHumanReadableString(ENGLISH, divAsFraction = true)) + .isEqualTo("negative 1 third") + + val exp18 = parseNumericExpressionWithAllErrors("-2/3") + assertThat(exp18.toHumanReadableString(ENGLISH, divAsFraction = true)) + .isEqualTo("negative 2 thirds") + + val exp19 = parseNumericExpressionWithAllErrors("10/11") + assertThat(exp19.toHumanReadableString(ENGLISH, divAsFraction = true)).isEqualTo("10 over 11") + + val exp20 = parseNumericExpressionWithAllErrors("121/7986") + assertThat(exp20.toHumanReadableString(ENGLISH, divAsFraction = true)) + .isEqualTo("121 over 7,986") + + val exp21 = parseNumericExpressionWithAllErrors("8/7") + assertThat(exp21.toHumanReadableString(ENGLISH, divAsFraction = true)).isEqualTo("8 over 7") + + val exp22 = parseNumericExpressionWithAllErrors("-10/-30") + assertThat(exp22.toHumanReadableString(ENGLISH, divAsFraction = true)) + .isEqualTo("negative the fraction with numerator 10 and denominator negative 30") + + val exp23 = parseAlgebraicExpressionWithAllErrors("1") + assertThat(exp23.toHumanReadableString(ENGLISH)).isEqualTo("1") + + val exp24 = parseAlgebraicExpressionWithoutOptionalErrors("((1))") + assertThat(exp24.toHumanReadableString(ENGLISH)).isEqualTo("1") + + val exp25 = parseAlgebraicExpressionWithAllErrors("x") + assertThat(exp25.toHumanReadableString(ENGLISH)).isEqualTo("x") + + val exp26 = parseAlgebraicExpressionWithoutOptionalErrors("((x))") + assertThat(exp26.toHumanReadableString(ENGLISH)).isEqualTo("x") + + val exp51 = parseAlgebraicExpressionWithAllErrors("-x") + assertThat(exp51.toHumanReadableString(ENGLISH)).isEqualTo("negative x") + + val exp52 = parseAlgebraicExpressionWithAllErrors("+x") + assertThat(exp52.toHumanReadableString(ENGLISH)).isEqualTo("positive x") + + val exp27 = parseAlgebraicExpressionWithAllErrors("1+x") + assertThat(exp27.toHumanReadableString(ENGLISH)).isEqualTo("1 plus x") + + val exp28 = parseAlgebraicExpressionWithAllErrors("1-x") + assertThat(exp28.toHumanReadableString(ENGLISH)).isEqualTo("1 minus x") + + val exp29 = parseAlgebraicExpressionWithAllErrors("1*x") + assertThat(exp29.toHumanReadableString(ENGLISH)).isEqualTo("1 times x") + + val exp30 = parseAlgebraicExpressionWithAllErrors("1/x") + assertThat(exp30.toHumanReadableString(ENGLISH)).isEqualTo("1 divided by x") + + val exp31 = parseAlgebraicExpressionWithAllErrors("1/x") + assertThat(exp31.toHumanReadableString(ENGLISH, divAsFraction = true)) + .isEqualTo("the fraction with numerator 1 and denominator x") + + val exp32 = parseAlgebraicExpressionWithAllErrors("1+(1-x)") + assertThat(exp32.toHumanReadableString(ENGLISH)) + .isEqualTo("1 plus open parenthesis 1 minus x close parenthesis") + + val exp33 = parseAlgebraicExpressionWithAllErrors("2x") + assertThat(exp33.toHumanReadableString(ENGLISH)).isEqualTo("2 x") + + val exp34 = parseAlgebraicExpressionWithAllErrors("xy") + assertThat(exp34.toHumanReadableString(ENGLISH)).isEqualTo("x times y") + + val exp35 = parseAlgebraicExpressionWithAllErrors("z") + assertThat(exp35.toHumanReadableString(ENGLISH)).isEqualTo("zed") + + val exp36 = parseAlgebraicExpressionWithAllErrors("2xz") + assertThat(exp36.toHumanReadableString(ENGLISH)).isEqualTo("2 x times zed") + + val exp37 = parseAlgebraicExpressionWithAllErrors("x^2") + assertThat(exp37.toHumanReadableString(ENGLISH)).isEqualTo("x raised to the power of 2") + + val exp38 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1+x)") + assertThat(exp38.toHumanReadableString(ENGLISH)) + .isEqualTo("x raised to the power of open parenthesis 1 plus x close parenthesis") + + val exp39 = parseAlgebraicExpressionWithAllErrors("100000*2") + assertThat(exp39.toHumanReadableString(ENGLISH)).isEqualTo("100,000 times 2") + + val exp40 = parseAlgebraicExpressionWithAllErrors("sqrt(2)") + assertThat(exp40.toHumanReadableString(ENGLISH)).isEqualTo("square root of 2") + + val exp41 = parseAlgebraicExpressionWithAllErrors("sqrt(x)") + assertThat(exp41.toHumanReadableString(ENGLISH)).isEqualTo("square root of x") + + val exp42 = parseAlgebraicExpressionWithAllErrors("√2") + assertThat(exp42.toHumanReadableString(ENGLISH)).isEqualTo("square root of 2") + + val exp43 = parseAlgebraicExpressionWithAllErrors("√x") + assertThat(exp43.toHumanReadableString(ENGLISH)).isEqualTo("square root of x") + + val exp44 = parseAlgebraicExpressionWithAllErrors("sqrt(1+2)") + assertThat(exp44.toHumanReadableString(ENGLISH)) + .isEqualTo("start square root 1 plus 2 end square root") + + val exp45 = parseAlgebraicExpressionWithAllErrors("sqrt(1+x)") + assertThat(exp45.toHumanReadableString(ENGLISH)) + .isEqualTo("start square root 1 plus x end square root") + + val exp46 = parseAlgebraicExpressionWithAllErrors("√(1+x)") + assertThat(exp46.toHumanReadableString(ENGLISH)) + .isEqualTo("start square root open parenthesis 1 plus x close parenthesis end square root") + + for (denominatorToCheck in 1..10) { + for (numeratorToCheck in 0..denominatorToCheck) { + val exp16 = parseAlgebraicExpressionWithAllErrors("$numeratorToCheck/$denominatorToCheck") + + val ordinalName = + if (numeratorToCheck == 1) { + singularOrdinalNames.getValue(denominatorToCheck) + } else pluralOrdinalNames.getValue(denominatorToCheck) + assertThat(exp16.toHumanReadableString(ENGLISH, divAsFraction = true)) + .isEqualTo("$numeratorToCheck $ordinalName") + } + } + + val exp47 = parseAlgebraicExpressionWithAllErrors("1") + assertThat(exp47.toHumanReadableString(ENGLISH)).isEqualTo("1") + + val exp48 = parseAlgebraicExpressionWithAllErrors("x(5-y)") + assertThat(exp48.toHumanReadableString(ENGLISH)) + .isEqualTo("x times open parenthesis 5 minus y close parenthesis") + + val eq2 = parseAlgebraicEquationWithAllErrors("x=1/y") + assertThat(eq2.toHumanReadableString(ENGLISH)).isEqualTo("x equals 1 divided by y") + + val eq3 = parseAlgebraicEquationWithAllErrors("x=1/2") + assertThat(eq3.toHumanReadableString(ENGLISH)).isEqualTo("x equals 1 divided by 2") + + val eq4 = parseAlgebraicEquationWithAllErrors("x=1/y") + assertThat(eq4.toHumanReadableString(ENGLISH, divAsFraction = true)) + .isEqualTo("x equals the fraction with numerator 1 and denominator y") + + val eq5 = parseAlgebraicEquationWithAllErrors("x=1/2") + assertThat(eq5.toHumanReadableString(ENGLISH, divAsFraction = true)) + .isEqualTo("x equals 1 half") + + // Tests from examples in the PRD + val eq6 = parseAlgebraicEquationWithAllErrors("3x^2+4y=62") + assertThat(eq6.toHumanReadableString(ENGLISH)) + .isEqualTo("3 x raised to the power of 2 plus 4 y equals 62") + + val exp53 = parseAlgebraicExpressionWithAllErrors("(x+6)/(x-4)") + assertThat(exp53.toHumanReadableString(ENGLISH, divAsFraction = true)) + .isEqualTo( + "the fraction with numerator open parenthesis x plus 6 close parenthesis and denominator" + + " open parenthesis x minus 4 close parenthesis" + ) + + val exp54 = parseAlgebraicExpressionWithoutOptionalErrors("4*(x)^(2)+20x") + assertThat(exp54.toHumanReadableString(ENGLISH)) + .isEqualTo("4 times x raised to the power of 2 plus 20 x") + + val exp55 = parseAlgebraicExpressionWithAllErrors("3+x-5") + assertThat(exp55.toHumanReadableString(ENGLISH)).isEqualTo("3 plus x minus 5") + + val exp56 = parseAlgebraicExpressionWithAllErrors("Z+A-Z", allowedVariables = listOf("A", "Z")) + assertThat(exp56.toHumanReadableString(ENGLISH)).isEqualTo("Zed plus A minus Zed") + + val exp57 = + parseAlgebraicExpressionWithAllErrors("6C-5A-1", allowedVariables = listOf("A", "C")) + assertThat(exp57.toHumanReadableString(ENGLISH)).isEqualTo("6 C minus 5 A minus 1") + + val exp58 = parseAlgebraicExpressionWithAllErrors("5*Z-w", allowedVariables = listOf("Z", "w")) + assertThat(exp58.toHumanReadableString(ENGLISH)).isEqualTo("5 times Zed minus w") + + val exp59 = + parseAlgebraicExpressionWithAllErrors("L*S-3S+L", allowedVariables = listOf("L", "S")) + assertThat(exp59.toHumanReadableString(ENGLISH)).isEqualTo("L times S minus 3 S plus L") + + val exp60 = parseAlgebraicExpressionWithAllErrors("2*(2+6+3+4)") + assertThat(exp60.toHumanReadableString(ENGLISH)) + .isEqualTo("2 times open parenthesis 2 plus 6 plus 3 plus 4 close parenthesis") + + val exp61 = parseAlgebraicExpressionWithAllErrors("sqrt(64)") + assertThat(exp61.toHumanReadableString(ENGLISH)).isEqualTo("square root of 64") + + val exp62 = parseAlgebraicExpressionWithAllErrors("√(a+b)", allowedVariables = listOf("a", "b")) + assertThat(exp62.toHumanReadableString(ENGLISH)) + .isEqualTo("start square root open parenthesis a plus b close parenthesis end square root") + + val exp63 = parseAlgebraicExpressionWithAllErrors("3*10^-5") + assertThat(exp63.toHumanReadableString(ENGLISH)) + .isEqualTo("3 times 10 raised to the power of negative 5") + + val exp64 = + parseAlgebraicExpressionWithoutOptionalErrors( + "((x+2y)+5*(a-2b)+z)", allowedVariables = listOf("x", "y", "a", "b", "z") + ) + assertThat(exp64.toHumanReadableString(ENGLISH)) + .isEqualTo( + "open parenthesis open parenthesis x plus 2 y close parenthesis plus 5 times open" + + " parenthesis a minus 2 b close parenthesis plus zed close parenthesis" + ) + } + @DslMarker private annotation class ExpressionComparatorMarker From 0d56e15f6da294ae21ed500d95b54db1d515cb04 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 7 Dec 2021 15:09:01 -0800 Subject: [PATCH 085/289] Some API & test cleanup for LaTeX & a11y strings. --- .../util/math/MathExpressionExtensions.kt | 26 +- .../util/math/NumericExpressionParserTest.kt | 339 ++++++++++++------ 2 files changed, 245 insertions(+), 120 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index e05f81a5fd8..8c91e663798 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -80,10 +80,7 @@ private val pluralOrdinalNames = mapOf( 10 to "tenths", ) -fun MathEquation.toHumanReadableString( - language: OppiaLanguage, - divAsFraction: Boolean = false -): String? { +fun MathEquation.toHumanReadableString(language: OppiaLanguage, divAsFraction: Boolean): String? { return when (language) { ENGLISH -> toHumanReadableEnglishString(divAsFraction) ARABIC, HINDI, HINGLISH, PORTUGUESE, BRAZILIAN_PORTUGUESE, LANGUAGE_UNSPECIFIED, UNRECOGNIZED -> @@ -91,10 +88,7 @@ fun MathEquation.toHumanReadableString( } } -fun MathExpression.toHumanReadableString( - language: OppiaLanguage, - divAsFraction: Boolean = false -): String? { +fun MathExpression.toHumanReadableString(language: OppiaLanguage, divAsFraction: Boolean): String? { return when (language) { ENGLISH -> toHumanReadableEnglishString(divAsFraction) ARABIC, HINDI, HINGLISH, PORTUGUESE, BRAZILIAN_PORTUGUESE, LANGUAGE_UNSPECIFIED, UNRECOGNIZED -> @@ -205,19 +199,17 @@ private fun MathExpression.isSingleTerm(): Boolean = when (expressionTypeCase) { EXPRESSIONTYPE_NOT_SET, null -> false } -fun MathEquation.toRawLatex(divAsFraction: Boolean = false): String { +fun MathEquation.toRawLatex(divAsFraction: Boolean): String { return "${leftSide.toRawLatex(divAsFraction)} = ${rightSide.toRawLatex(divAsFraction)}" } -fun MathExpression.toRawLatex(divAsFraction: Boolean = false): String = toRawLatexAux(divAsFraction) - -private fun MathExpression.toRawLatexAux(divAsFraction: Boolean): String { +fun MathExpression.toRawLatex(divAsFraction: Boolean): String { return when (expressionTypeCase) { CONSTANT -> constant.toPlainString() VARIABLE -> variable BINARY_OPERATION -> { - val lhsLatex = binaryOperation.leftOperand.toRawLatexAux(divAsFraction) - val rhsLatex = binaryOperation.rightOperand.toRawLatexAux(divAsFraction) + val lhsLatex = binaryOperation.leftOperand.toRawLatex(divAsFraction) + val rhsLatex = binaryOperation.rightOperand.toRawLatex(divAsFraction) when (binaryOperation.operator) { BinaryOperator.ADD -> "$lhsLatex + $rhsLatex" BinaryOperator.SUBTRACT -> "$lhsLatex - $rhsLatex" @@ -233,7 +225,7 @@ private fun MathExpression.toRawLatexAux(divAsFraction: Boolean): String { } } UNARY_OPERATION -> { - val operandLatex = unaryOperation.operand.toRawLatexAux(divAsFraction) + val operandLatex = unaryOperation.operand.toRawLatex(divAsFraction) when (unaryOperation.operator) { UnaryOperator.NEGATE -> "-$operandLatex" UnaryOperator.POSITIVE -> "+$operandLatex" @@ -241,13 +233,13 @@ private fun MathExpression.toRawLatexAux(divAsFraction: Boolean): String { } } FUNCTION_CALL -> { - val argumentLatex = functionCall.argument.toRawLatexAux(divAsFraction) + val argumentLatex = functionCall.argument.toRawLatex(divAsFraction) when (functionCall.functionType) { FunctionType.SQUARE_ROOT -> "\\sqrt{$argumentLatex}" FunctionType.FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> argumentLatex } } - GROUP -> "(${group.toRawLatexAux(divAsFraction)})" + GROUP -> "(${group.toRawLatex(divAsFraction)})" EXPRESSIONTYPE_NOT_SET, null -> "" } } diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index f89c5f5ddbd..60923595bcb 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -27,6 +27,7 @@ import org.robolectric.annotation.LooperMode import kotlin.math.sqrt import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.OppiaLanguage import org.oppia.android.app.model.OppiaLanguage.ARABIC import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE import org.oppia.android.app.model.OppiaLanguage.ENGLISH @@ -4226,49 +4227,51 @@ class NumericExpressionParserTest { // TODO: split up & move to separate test suites. Finish test cases. val exp1 = parseNumericExpressionWithAllErrors("1") - assertThat(exp1.toRawLatex()).isEqualTo("1") + assertThat(exp1).convertsToLatexStringThat().isEqualTo("1") val exp2 = parseNumericExpressionWithAllErrors("1+2") - assertThat(exp2.toRawLatex()).isEqualTo("1 + 2") + assertThat(exp2).convertsToLatexStringThat().isEqualTo("1 + 2") val exp3 = parseNumericExpressionWithAllErrors("1*2") - assertThat(exp3.toRawLatex()).isEqualTo("1 \\times 2") + assertThat(exp3).convertsToLatexStringThat().isEqualTo("1 \\times 2") val exp4 = parseNumericExpressionWithAllErrors("1/2") - assertThat(exp4.toRawLatex()).isEqualTo("1 \\div 2") + assertThat(exp4).convertsToLatexStringThat().isEqualTo("1 \\div 2") val exp5 = parseNumericExpressionWithAllErrors("1/2") - assertThat(exp5.toRawLatex(divAsFraction = true)).isEqualTo("\\frac{1}{2}") + assertThat(exp5).convertsWithFractionsToLatexStringThat().isEqualTo("\\frac{1}{2}") val exp10 = parseNumericExpressionWithAllErrors("√2") - assertThat(exp10.toRawLatex()).isEqualTo("\\sqrt{2}") + assertThat(exp10).convertsToLatexStringThat().isEqualTo("\\sqrt{2}") val exp11 = parseNumericExpressionWithAllErrors("√(1/2)") - assertThat(exp11.toRawLatex()).isEqualTo("\\sqrt{(1 \\div 2)}") + assertThat(exp11).convertsToLatexStringThat().isEqualTo("\\sqrt{(1 \\div 2)}") val exp6 = parseAlgebraicExpressionWithAllErrors("x+y") - assertThat(exp6.toRawLatex()).isEqualTo("x + y") + assertThat(exp6).convertsToLatexStringThat().isEqualTo("x + y") val exp7 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1/y)") - assertThat(exp7.toRawLatex()).isEqualTo("x ^ {(1 \\div y)}") + assertThat(exp7).convertsToLatexStringThat().isEqualTo("x ^ {(1 \\div y)}") val exp8 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1/y)") - assertThat(exp8.toRawLatex(divAsFraction = true)).isEqualTo("x ^ {(\\frac{1}{y})}") + assertThat(exp8).convertsWithFractionsToLatexStringThat().isEqualTo("x ^ {(\\frac{1}{y})}") val exp9 = parseAlgebraicExpressionWithoutOptionalErrors("x^y^z") - assertThat(exp9.toRawLatex(divAsFraction = true)).isEqualTo("x ^ {y ^ {z}}") + assertThat(exp9).convertsWithFractionsToLatexStringThat().isEqualTo("x ^ {y ^ {z}}") val eq1 = parseAlgebraicEquationWithAllErrors( "7a^2+b^2+c^2=0", allowedVariables = listOf("a", "b", "c") ) - assertThat(eq1.toRawLatex()).isEqualTo("7a ^ {2} + b ^ {2} + c ^ {2} = 0") + assertThat(eq1).convertsToLatexStringThat().isEqualTo("7a ^ {2} + b ^ {2} + c ^ {2} = 0") val eq2 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") - assertThat(eq2.toRawLatex()).isEqualTo("\\sqrt{1 + x} \\div x = 1") + assertThat(eq2).convertsToLatexStringThat().isEqualTo("\\sqrt{1 + x} \\div x = 1") val eq3 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") - assertThat(eq3.toRawLatex(divAsFraction = true)).isEqualTo("\\frac{\\sqrt{1 + x}}{x} = 1") + assertThat(eq3) + .convertsWithFractionsToLatexStringThat() + .isEqualTo("\\frac{\\sqrt{1 + x}}{x} = 1") } @Test @@ -4276,97 +4279,107 @@ class NumericExpressionParserTest { // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). val exp1 = parseNumericExpressionWithAllErrors("1") - assertThat(exp1.toHumanReadableString(ARABIC)).isNull() + assertThat(exp1).forHumanReadable(ARABIC).doesNotConvertToString() - assertThat(exp1.toHumanReadableString(HINDI)).isNull() + assertThat(exp1).forHumanReadable(HINDI).doesNotConvertToString() - assertThat(exp1.toHumanReadableString(HINGLISH)).isNull() + assertThat(exp1).forHumanReadable(HINGLISH).doesNotConvertToString() - assertThat(exp1.toHumanReadableString(PORTUGUESE)).isNull() + assertThat(exp1).forHumanReadable(PORTUGUESE).doesNotConvertToString() - assertThat(exp1.toHumanReadableString(BRAZILIAN_PORTUGUESE)).isNull() + assertThat(exp1).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() - assertThat(exp1.toHumanReadableString(LANGUAGE_UNSPECIFIED)).isNull() + assertThat(exp1).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() - assertThat(exp1.toHumanReadableString(UNRECOGNIZED)).isNull() + assertThat(exp1).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() val exp2 = parseAlgebraicExpressionWithAllErrors("x") - assertThat(exp2.toHumanReadableString(ARABIC)).isNull() + assertThat(exp2).forHumanReadable(ARABIC).doesNotConvertToString() - assertThat(exp2.toHumanReadableString(HINDI)).isNull() + assertThat(exp2).forHumanReadable(HINDI).doesNotConvertToString() - assertThat(exp2.toHumanReadableString(HINGLISH)).isNull() + assertThat(exp2).forHumanReadable(HINGLISH).doesNotConvertToString() - assertThat(exp2.toHumanReadableString(PORTUGUESE)).isNull() + assertThat(exp2).forHumanReadable(PORTUGUESE).doesNotConvertToString() - assertThat(exp2.toHumanReadableString(BRAZILIAN_PORTUGUESE)).isNull() + assertThat(exp2).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() - assertThat(exp2.toHumanReadableString(LANGUAGE_UNSPECIFIED)).isNull() + assertThat(exp2).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() - assertThat(exp2.toHumanReadableString(UNRECOGNIZED)).isNull() + assertThat(exp2).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() val eq1 = parseAlgebraicEquationWithAllErrors("x=1") - assertThat(eq1.toHumanReadableString(ARABIC)).isNull() + assertThat(eq1).forHumanReadable(ARABIC).doesNotConvertToString() - assertThat(eq1.toHumanReadableString(HINDI)).isNull() + assertThat(eq1).forHumanReadable(HINDI).doesNotConvertToString() - assertThat(eq1.toHumanReadableString(HINGLISH)).isNull() + assertThat(eq1).forHumanReadable(HINGLISH).doesNotConvertToString() - assertThat(eq1.toHumanReadableString(PORTUGUESE)).isNull() + assertThat(eq1).forHumanReadable(PORTUGUESE).doesNotConvertToString() - assertThat(eq1.toHumanReadableString(BRAZILIAN_PORTUGUESE)).isNull() + assertThat(eq1).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() - assertThat(eq1.toHumanReadableString(LANGUAGE_UNSPECIFIED)).isNull() + assertThat(eq1).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() - assertThat(eq1.toHumanReadableString(UNRECOGNIZED)).isNull() + assertThat(eq1).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() // specific cases (from rules & other cases): val exp3 = parseNumericExpressionWithAllErrors("1") - assertThat(exp3.toHumanReadableString(ENGLISH)).isEqualTo("1") + assertThat(exp3).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + assertThat(exp3).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") val exp49 = parseNumericExpressionWithAllErrors("-1") - assertThat(exp49.toHumanReadableString(ENGLISH)).isEqualTo("negative 1") + assertThat(exp49).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative 1") val exp50 = parseNumericExpressionWithAllErrors("+1") - assertThat(exp50.toHumanReadableString(ENGLISH)).isEqualTo("positive 1") + assertThat(exp50).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive 1") val exp4 = parseNumericExpressionWithoutOptionalErrors("((1))") - assertThat(exp4.toHumanReadableString(ENGLISH)).isEqualTo("1") + assertThat(exp4).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") val exp5 = parseNumericExpressionWithAllErrors("1+2") - assertThat(exp5.toHumanReadableString(ENGLISH)).isEqualTo("1 plus 2") + assertThat(exp5).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus 2") val exp6 = parseNumericExpressionWithAllErrors("1-2") - assertThat(exp6.toHumanReadableString(ENGLISH)).isEqualTo("1 minus 2") + assertThat(exp6).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus 2") val exp7 = parseNumericExpressionWithAllErrors("1*2") - assertThat(exp7.toHumanReadableString(ENGLISH)).isEqualTo("1 times 2") + assertThat(exp7).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times 2") val exp8 = parseNumericExpressionWithAllErrors("1/2") - assertThat(exp8.toHumanReadableString(ENGLISH)).isEqualTo("1 divided by 2") + assertThat(exp8).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by 2") val exp9 = parseNumericExpressionWithAllErrors("1+(1-2)") - assertThat(exp9.toHumanReadableString(ENGLISH)) + assertThat(exp9) + .forHumanReadable(ENGLISH) + .convertsToStringThat() .isEqualTo("1 plus open parenthesis 1 minus 2 close parenthesis") val exp10 = parseNumericExpressionWithAllErrors("2^3") - assertThat(exp10.toHumanReadableString(ENGLISH)).isEqualTo("2 raised to the power of 3") + assertThat(exp10) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("2 raised to the power of 3") val exp11 = parseNumericExpressionWithAllErrors("2^(1+2)") - assertThat(exp11.toHumanReadableString(ENGLISH)) + assertThat(exp11) + .forHumanReadable(ENGLISH) + .convertsToStringThat() .isEqualTo("2 raised to the power of open parenthesis 1 plus 2 close parenthesis") val exp12 = parseNumericExpressionWithAllErrors("100000*2") - assertThat(exp12.toHumanReadableString(ENGLISH)).isEqualTo("100,000 times 2") + assertThat(exp12).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") val exp13 = parseNumericExpressionWithAllErrors("sqrt(2)") - assertThat(exp13.toHumanReadableString(ENGLISH)).isEqualTo("square root of 2") + assertThat(exp13).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") val exp14 = parseNumericExpressionWithAllErrors("√2") - assertThat(exp14.toHumanReadableString(ENGLISH)).isEqualTo("square root of 2") + assertThat(exp14).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") val exp15 = parseNumericExpressionWithAllErrors("sqrt(1+2)") - assertThat(exp15.toHumanReadableString(ENGLISH)) + assertThat(exp15) + .forHumanReadable(ENGLISH) + .convertsToStringThat() .isEqualTo("start square root 1 plus 2 end square root") val singularOrdinalNames = mapOf( @@ -4401,115 +4414,146 @@ class NumericExpressionParserTest { if (numeratorToCheck == 1) { singularOrdinalNames.getValue(denominatorToCheck) } else pluralOrdinalNames.getValue(denominatorToCheck) - assertThat(exp16.toHumanReadableString(ENGLISH, divAsFraction = true)) + assertThat(exp16) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() .isEqualTo("$numeratorToCheck $ordinalName") } } val exp17 = parseNumericExpressionWithAllErrors("-1/3") - assertThat(exp17.toHumanReadableString(ENGLISH, divAsFraction = true)) + assertThat(exp17) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() .isEqualTo("negative 1 third") val exp18 = parseNumericExpressionWithAllErrors("-2/3") - assertThat(exp18.toHumanReadableString(ENGLISH, divAsFraction = true)) + assertThat(exp18) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() .isEqualTo("negative 2 thirds") val exp19 = parseNumericExpressionWithAllErrors("10/11") - assertThat(exp19.toHumanReadableString(ENGLISH, divAsFraction = true)).isEqualTo("10 over 11") + assertThat(exp19) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("10 over 11") val exp20 = parseNumericExpressionWithAllErrors("121/7986") - assertThat(exp20.toHumanReadableString(ENGLISH, divAsFraction = true)) + assertThat(exp20) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() .isEqualTo("121 over 7,986") val exp21 = parseNumericExpressionWithAllErrors("8/7") - assertThat(exp21.toHumanReadableString(ENGLISH, divAsFraction = true)).isEqualTo("8 over 7") + assertThat(exp21) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("8 over 7") val exp22 = parseNumericExpressionWithAllErrors("-10/-30") - assertThat(exp22.toHumanReadableString(ENGLISH, divAsFraction = true)) + assertThat(exp22) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() .isEqualTo("negative the fraction with numerator 10 and denominator negative 30") val exp23 = parseAlgebraicExpressionWithAllErrors("1") - assertThat(exp23.toHumanReadableString(ENGLISH)).isEqualTo("1") + assertThat(exp23).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") val exp24 = parseAlgebraicExpressionWithoutOptionalErrors("((1))") - assertThat(exp24.toHumanReadableString(ENGLISH)).isEqualTo("1") + assertThat(exp24).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") val exp25 = parseAlgebraicExpressionWithAllErrors("x") - assertThat(exp25.toHumanReadableString(ENGLISH)).isEqualTo("x") + assertThat(exp25).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") val exp26 = parseAlgebraicExpressionWithoutOptionalErrors("((x))") - assertThat(exp26.toHumanReadableString(ENGLISH)).isEqualTo("x") + assertThat(exp26).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") val exp51 = parseAlgebraicExpressionWithAllErrors("-x") - assertThat(exp51.toHumanReadableString(ENGLISH)).isEqualTo("negative x") + assertThat(exp51).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative x") val exp52 = parseAlgebraicExpressionWithAllErrors("+x") - assertThat(exp52.toHumanReadableString(ENGLISH)).isEqualTo("positive x") + assertThat(exp52).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive x") val exp27 = parseAlgebraicExpressionWithAllErrors("1+x") - assertThat(exp27.toHumanReadableString(ENGLISH)).isEqualTo("1 plus x") + assertThat(exp27).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus x") val exp28 = parseAlgebraicExpressionWithAllErrors("1-x") - assertThat(exp28.toHumanReadableString(ENGLISH)).isEqualTo("1 minus x") + assertThat(exp28).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus x") val exp29 = parseAlgebraicExpressionWithAllErrors("1*x") - assertThat(exp29.toHumanReadableString(ENGLISH)).isEqualTo("1 times x") + assertThat(exp29).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times x") val exp30 = parseAlgebraicExpressionWithAllErrors("1/x") - assertThat(exp30.toHumanReadableString(ENGLISH)).isEqualTo("1 divided by x") + assertThat(exp30).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by x") val exp31 = parseAlgebraicExpressionWithAllErrors("1/x") - assertThat(exp31.toHumanReadableString(ENGLISH, divAsFraction = true)) + assertThat(exp31) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() .isEqualTo("the fraction with numerator 1 and denominator x") val exp32 = parseAlgebraicExpressionWithAllErrors("1+(1-x)") - assertThat(exp32.toHumanReadableString(ENGLISH)) + assertThat(exp32) + .forHumanReadable(ENGLISH) + .convertsToStringThat() .isEqualTo("1 plus open parenthesis 1 minus x close parenthesis") val exp33 = parseAlgebraicExpressionWithAllErrors("2x") - assertThat(exp33.toHumanReadableString(ENGLISH)).isEqualTo("2 x") + assertThat(exp33).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x") val exp34 = parseAlgebraicExpressionWithAllErrors("xy") - assertThat(exp34.toHumanReadableString(ENGLISH)).isEqualTo("x times y") + assertThat(exp34).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x times y") val exp35 = parseAlgebraicExpressionWithAllErrors("z") - assertThat(exp35.toHumanReadableString(ENGLISH)).isEqualTo("zed") + assertThat(exp35).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("zed") val exp36 = parseAlgebraicExpressionWithAllErrors("2xz") - assertThat(exp36.toHumanReadableString(ENGLISH)).isEqualTo("2 x times zed") + assertThat(exp36).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x times zed") val exp37 = parseAlgebraicExpressionWithAllErrors("x^2") - assertThat(exp37.toHumanReadableString(ENGLISH)).isEqualTo("x raised to the power of 2") + assertThat(exp37) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x raised to the power of 2") val exp38 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1+x)") - assertThat(exp38.toHumanReadableString(ENGLISH)) + assertThat(exp38) + .forHumanReadable(ENGLISH) + .convertsToStringThat() .isEqualTo("x raised to the power of open parenthesis 1 plus x close parenthesis") val exp39 = parseAlgebraicExpressionWithAllErrors("100000*2") - assertThat(exp39.toHumanReadableString(ENGLISH)).isEqualTo("100,000 times 2") + assertThat(exp39).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") val exp40 = parseAlgebraicExpressionWithAllErrors("sqrt(2)") - assertThat(exp40.toHumanReadableString(ENGLISH)).isEqualTo("square root of 2") + assertThat(exp40).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") val exp41 = parseAlgebraicExpressionWithAllErrors("sqrt(x)") - assertThat(exp41.toHumanReadableString(ENGLISH)).isEqualTo("square root of x") + assertThat(exp41).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") val exp42 = parseAlgebraicExpressionWithAllErrors("√2") - assertThat(exp42.toHumanReadableString(ENGLISH)).isEqualTo("square root of 2") + assertThat(exp42).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") val exp43 = parseAlgebraicExpressionWithAllErrors("√x") - assertThat(exp43.toHumanReadableString(ENGLISH)).isEqualTo("square root of x") + assertThat(exp43).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") val exp44 = parseAlgebraicExpressionWithAllErrors("sqrt(1+2)") - assertThat(exp44.toHumanReadableString(ENGLISH)) + assertThat(exp44) + .forHumanReadable(ENGLISH) + .convertsToStringThat() .isEqualTo("start square root 1 plus 2 end square root") val exp45 = parseAlgebraicExpressionWithAllErrors("sqrt(1+x)") - assertThat(exp45.toHumanReadableString(ENGLISH)) + assertThat(exp45) + .forHumanReadable(ENGLISH) + .convertsToStringThat() .isEqualTo("start square root 1 plus x end square root") val exp46 = parseAlgebraicExpressionWithAllErrors("√(1+x)") - assertThat(exp46.toHumanReadableString(ENGLISH)) + assertThat(exp46) + .forHumanReadable(ENGLISH) + .convertsToStringThat() .isEqualTo("start square root open parenthesis 1 plus x close parenthesis end square root") for (denominatorToCheck in 1..10) { @@ -4520,85 +4564,127 @@ class NumericExpressionParserTest { if (numeratorToCheck == 1) { singularOrdinalNames.getValue(denominatorToCheck) } else pluralOrdinalNames.getValue(denominatorToCheck) - assertThat(exp16.toHumanReadableString(ENGLISH, divAsFraction = true)) + assertThat(exp16) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() .isEqualTo("$numeratorToCheck $ordinalName") } } val exp47 = parseAlgebraicExpressionWithAllErrors("1") - assertThat(exp47.toHumanReadableString(ENGLISH)).isEqualTo("1") + assertThat(exp47).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") val exp48 = parseAlgebraicExpressionWithAllErrors("x(5-y)") - assertThat(exp48.toHumanReadableString(ENGLISH)) + assertThat(exp48) + .forHumanReadable(ENGLISH) + .convertsToStringThat() .isEqualTo("x times open parenthesis 5 minus y close parenthesis") val eq2 = parseAlgebraicEquationWithAllErrors("x=1/y") - assertThat(eq2.toHumanReadableString(ENGLISH)).isEqualTo("x equals 1 divided by y") + assertThat(eq2) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x equals 1 divided by y") val eq3 = parseAlgebraicEquationWithAllErrors("x=1/2") - assertThat(eq3.toHumanReadableString(ENGLISH)).isEqualTo("x equals 1 divided by 2") + assertThat(eq3) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x equals 1 divided by 2") val eq4 = parseAlgebraicEquationWithAllErrors("x=1/y") - assertThat(eq4.toHumanReadableString(ENGLISH, divAsFraction = true)) + assertThat(eq4) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() .isEqualTo("x equals the fraction with numerator 1 and denominator y") val eq5 = parseAlgebraicEquationWithAllErrors("x=1/2") - assertThat(eq5.toHumanReadableString(ENGLISH, divAsFraction = true)) + assertThat(eq5) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() .isEqualTo("x equals 1 half") // Tests from examples in the PRD val eq6 = parseAlgebraicEquationWithAllErrors("3x^2+4y=62") - assertThat(eq6.toHumanReadableString(ENGLISH)) + assertThat(eq6) + .forHumanReadable(ENGLISH) + .convertsToStringThat() .isEqualTo("3 x raised to the power of 2 plus 4 y equals 62") val exp53 = parseAlgebraicExpressionWithAllErrors("(x+6)/(x-4)") - assertThat(exp53.toHumanReadableString(ENGLISH, divAsFraction = true)) + assertThat(exp53) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() .isEqualTo( "the fraction with numerator open parenthesis x plus 6 close parenthesis and denominator" + " open parenthesis x minus 4 close parenthesis" ) val exp54 = parseAlgebraicExpressionWithoutOptionalErrors("4*(x)^(2)+20x") - assertThat(exp54.toHumanReadableString(ENGLISH)) + assertThat(exp54) + .forHumanReadable(ENGLISH) + .convertsToStringThat() .isEqualTo("4 times x raised to the power of 2 plus 20 x") val exp55 = parseAlgebraicExpressionWithAllErrors("3+x-5") - assertThat(exp55.toHumanReadableString(ENGLISH)).isEqualTo("3 plus x minus 5") + assertThat(exp55).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("3 plus x minus 5") val exp56 = parseAlgebraicExpressionWithAllErrors("Z+A-Z", allowedVariables = listOf("A", "Z")) - assertThat(exp56.toHumanReadableString(ENGLISH)).isEqualTo("Zed plus A minus Zed") + assertThat(exp56).forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("Zed plus A minus Zed") val exp57 = parseAlgebraicExpressionWithAllErrors("6C-5A-1", allowedVariables = listOf("A", "C")) - assertThat(exp57.toHumanReadableString(ENGLISH)).isEqualTo("6 C minus 5 A minus 1") + assertThat(exp57) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("6 C minus 5 A minus 1") val exp58 = parseAlgebraicExpressionWithAllErrors("5*Z-w", allowedVariables = listOf("Z", "w")) - assertThat(exp58.toHumanReadableString(ENGLISH)).isEqualTo("5 times Zed minus w") + assertThat(exp58) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("5 times Zed minus w") val exp59 = parseAlgebraicExpressionWithAllErrors("L*S-3S+L", allowedVariables = listOf("L", "S")) - assertThat(exp59.toHumanReadableString(ENGLISH)).isEqualTo("L times S minus 3 S plus L") + assertThat(exp59) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("L times S minus 3 S plus L") val exp60 = parseAlgebraicExpressionWithAllErrors("2*(2+6+3+4)") - assertThat(exp60.toHumanReadableString(ENGLISH)) + assertThat(exp60) + .forHumanReadable(ENGLISH) + .convertsToStringThat() .isEqualTo("2 times open parenthesis 2 plus 6 plus 3 plus 4 close parenthesis") val exp61 = parseAlgebraicExpressionWithAllErrors("sqrt(64)") - assertThat(exp61.toHumanReadableString(ENGLISH)).isEqualTo("square root of 64") + assertThat(exp61) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("square root of 64") val exp62 = parseAlgebraicExpressionWithAllErrors("√(a+b)", allowedVariables = listOf("a", "b")) - assertThat(exp62.toHumanReadableString(ENGLISH)) + assertThat(exp62) + .forHumanReadable(ENGLISH) + .convertsToStringThat() .isEqualTo("start square root open parenthesis a plus b close parenthesis end square root") val exp63 = parseAlgebraicExpressionWithAllErrors("3*10^-5") - assertThat(exp63.toHumanReadableString(ENGLISH)) + assertThat(exp63) + .forHumanReadable(ENGLISH) + .convertsToStringThat() .isEqualTo("3 times 10 raised to the power of negative 5") val exp64 = parseAlgebraicExpressionWithoutOptionalErrors( "((x+2y)+5*(a-2b)+z)", allowedVariables = listOf("x", "y", "a", "b", "z") ) - assertThat(exp64.toHumanReadableString(ENGLISH)) + assertThat(exp64) + .forHumanReadable(ENGLISH) + .convertsToStringThat() .isEqualTo( "open parenthesis open parenthesis x plus 2 y close parenthesis plus 5 times open" + " parenthesis a minus 2 b close parenthesis plus zed close parenthesis" @@ -4627,6 +4713,15 @@ class NumericExpressionParserTest { fun evaluatesToIntegerThat(): IntegerSubject = assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.INTEGER).integer) + fun convertsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = false)) + + fun convertsWithFractionsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = true)) + + fun forHumanReadable(language: OppiaLanguage): HumanReadableStringChecker = + HumanReadableStringChecker(language, actual::toHumanReadableString) + private fun evaluateAsReal(expectedType: Real.RealTypeCase): Real { val real = actual.evaluateAsNumericExpression() assertWithMessage("Failed to evaluate numeric expression").that(real).isNotNull() @@ -4636,6 +4731,8 @@ class NumericExpressionParserTest { return checkNotNull(real) // Just to remove the nullable operator; the actual check is above. } + private fun convertToLatex(divAsFraction: Boolean): String = actual.toRawLatex(divAsFraction) + // TODO: update DSL to not have return values (since it's unnecessary). @ExpressionComparatorMarker class ExpressionComparator private constructor(private val expression: MathExpression) { @@ -4821,6 +4918,42 @@ class NumericExpressionParserTest { fun hasLeftHandSideThat(): MathExpressionSubject = assertThat(actual.leftSide) fun hasRightHandSideThat(): MathExpressionSubject = assertThat(actual.rightSide) + + fun convertsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = false)) + + fun convertsWithFractionsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = true)) + + fun forHumanReadable(language: OppiaLanguage): HumanReadableStringChecker = + HumanReadableStringChecker(language, actual::toHumanReadableString) + + private fun convertToLatex(divAsFraction: Boolean): String = actual.toRawLatex(divAsFraction) + } + + private class HumanReadableStringChecker( + private val language: OppiaLanguage, + private val maybeConvertToHumanReadableString: (OppiaLanguage, Boolean) -> String? + ) { + fun convertsToStringThat(): StringSubject = + assertThat(convertToHumanReadableString(language, /* divAsFraction= */ false)) + + fun convertsWithFractionsToStringThat(): StringSubject = + assertThat(convertToHumanReadableString(language, /* divAsFraction= */ true)) + + fun doesNotConvertToString() { + assertWithMessage("Expected to not convert to: $language") + .that(maybeConvertToHumanReadableString(language, /* divAsFraction= */ false)) + .isNull() + } + + private fun convertToHumanReadableString( + language: OppiaLanguage, divAsFraction: Boolean + ): String { + val readableString = maybeConvertToHumanReadableString(language, divAsFraction) + assertWithMessage("Expected to convert to: $language").that(readableString).isNotNull() + return checkNotNull(readableString) // Verified in the above assertion check. + } } // TODO: move this to a common location. From 0f84bca16764698102c140ab4717c13325480252 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 9 Dec 2021 12:55:29 -0800 Subject: [PATCH 086/289] Add support to compare expressions commutatively. --- model/src/main/proto/math.proto | 27 +- .../util/math/MathExpressionExtensions.kt | 333 +++ .../org/oppia/android/util/math/BUILD.bazel | 1 + .../util/math/NumericExpressionParserTest.kt | 2316 ++++++++++++++--- 4 files changed, 2306 insertions(+), 371 deletions(-) diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index c0ddd516723..68daa1a8cbc 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -39,11 +39,11 @@ message MathEquation { message MathBinaryOperation { enum Operator { OPERATOR_UNSPECIFIED = 0; - // Represents adding two values, e.g.: 1 + x. + // Represents adding two values, e.g.: 1+x. ADD = 1; - // Represents subtracting two values, e.g.: x - 2. + // Represents subtracting two values, e.g.: x-2. SUBTRACT = 2; - // Represents multiplying two values, e.g.: x * y. + // Represents multiplying two values, e.g.: x*y. MULTIPLY = 3; // Represents dividing two values, e.g.: 1/x. DIVIDE = 4; @@ -92,11 +92,13 @@ message Real { message ComparableOperationList { message ComparableOperation { + bool is_negated = 1; + oneof comparison_type { - CommutativeAccumulation commutative_accumulation = 1; - NonCommutativeOperation non_commutative_operation = 2; - Real constant_term = 3; - string variable_term = 4; + CommutativeAccumulation commutative_accumulation = 2; + NonCommutativeOperation non_commutative_operation = 3; + Real constant_term = 4; + string variable_term = 5; } } message CommutativeAccumulation { @@ -110,21 +112,16 @@ message ComparableOperationList { repeated ComparableOperation combined_operations = 2; } message NonCommutativeOperation { - bool is_negated = 1; - oneof operation_type { - BinaryOperation division = 2; - BinaryOperation exponentiation = 3; - UnaryOperation square_root = 4; + BinaryOperation division = 1; + BinaryOperation exponentiation = 2; + ComparableOperation square_root = 3; } message BinaryOperation { ComparableOperation left_operand = 1; ComparableOperation right_operand = 2; } - message UnaryOperation { - ComparableOperation operand = 1; - } } ComparableOperation root_operation = 1; diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 8c91e663798..f80184ecedf 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -2,6 +2,7 @@ package org.oppia.android.util.math import java.text.NumberFormat import java.util.Locale +import java.util.SortedSet import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.MathBinaryOperation import org.oppia.android.app.model.MathExpression @@ -24,6 +25,17 @@ import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import kotlin.math.abs import kotlin.math.pow import kotlin.math.sqrt +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.COMPARISONTYPE_NOT_SET +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.CONSTANT_TERM +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.VARIABLE_TERM +import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation +import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation.OperationTypeCase import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP @@ -53,6 +65,327 @@ import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED // TODO: Make sure that all 'when' cases here do not use 'else' branches to ensure structural // changes require changing logic. +private val COMPARABLE_OPERATION_COMPARATOR: Comparator by lazy { + // Some of the comparators must be deferred since they indirectly reference this comparator (which + // isn't valid until it's fully assembled). + Comparator.comparing(ComparableOperation::getComparisonTypeCase) + .thenComparing(ComparableOperation::getIsNegated) + .selectAmong( + ComparableOperation::getComparisonTypeCase, + COMMUTATIVE_ACCUMULATION to comparingDeferred( + ComparableOperation::getCommutativeAccumulation + ) { COMMUTATIVE_ACCUMULATION_COMPARATOR }, + NON_COMMUTATIVE_OPERATION to comparingDeferred( + ComparableOperation::getNonCommutativeOperation + ) { NON_COMMUTATIVE_OPERATION_COMPARATOR }, + CONSTANT_TERM to Comparator.comparing(ComparableOperation::getConstantTerm, REAL_COMPARATOR), + VARIABLE_TERM to Comparator.comparing(ComparableOperation::getVariableTerm) + ) +} + +private val COMMUTATIVE_ACCUMULATION_COMPARATOR: Comparator by lazy { + Comparator.comparing(CommutativeAccumulation::getAccumulationType) + .thenComparing({ accumulation -> + accumulation.combinedOperationsList.toSortedSet(COMPARABLE_OPERATION_COMPARATOR) + }, ::compareSets) +} + +private val NON_COMMUTATIVE_BINARY_OPERATION_COMPARATOR by lazy { + Comparator.comparing( + NonCommutativeOperation.BinaryOperation::getLeftOperand, COMPARABLE_OPERATION_COMPARATOR + ).thenComparing( + NonCommutativeOperation.BinaryOperation::getRightOperand, COMPARABLE_OPERATION_COMPARATOR + ) +} + +private val NON_COMMUTATIVE_OPERATION_COMPARATOR: Comparator by lazy { + Comparator.comparing(NonCommutativeOperation::getOperationTypeCase) + .selectAmong( + NonCommutativeOperation::getOperationTypeCase, + OperationTypeCase.DIVISION to Comparator.comparing( + NonCommutativeOperation::getDivision, NON_COMMUTATIVE_BINARY_OPERATION_COMPARATOR + ), + OperationTypeCase.EXPONENTIATION to Comparator.comparing( + NonCommutativeOperation::getExponentiation, NON_COMMUTATIVE_BINARY_OPERATION_COMPARATOR + ), + OperationTypeCase.SQUARE_ROOT to Comparator.comparing( + NonCommutativeOperation::getSquareRoot, COMPARABLE_OPERATION_COMPARATOR + ), + ) +} + +private val REAL_COMPARATOR: Comparator by lazy { Comparator.comparing(Real::toDouble) } + +private fun comparingDeferred( + keySelector: (T) -> U, comparatorSelector: () -> Comparator +): Comparator { + // Store as captured val for memoization. + val comparator by lazy { comparatorSelector() } + return Comparator.comparing(keySelector) { o1, o2 -> + comparator.compare(o1, o2) + } +} + +private fun > Comparator.selectAmong( + enumSelector: (T) -> E, vararg comparators: Pair> +): Comparator { + val comparatorMap = comparators.toMap() + return thenComparing( + Comparator { o1, o2 -> + val enum1 = enumSelector(o1) + val enum2 = enumSelector(o2) + check(enum1 == enum2) { + "Expected objects to have the same enum values: $o1 ($enum1), $o2 ($enum2)" + } + val comparator = checkNotNull(comparatorMap[enum1]) { "No comparator for matched enum: $enum1" } + return@Comparator comparator.compare(o1, o2) + } + ) +} + +private fun compareSets( + first: SortedSet, second: SortedSet +): Int { + // Reference: https://stackoverflow.com/a/30107086. + val firstIter = first.iterator() + val secondIter = second.iterator() + while (firstIter.hasNext() || secondIter.hasNext()) { + val comparison = COMPARABLE_OPERATION_COMPARATOR.compare(firstIter.next(), secondIter.next()) + if (comparison != 0) return comparison // Found a different item. + } + + // Everything is equal up to here, see if the lists are different length. + return when { + firstIter.hasNext() -> 1 // The first list is longer, therefore "greater." + secondIter.hasNext() -> -1 // Ditto, but for the second list. + else -> 0 // Otherwise, they're the same length with all equal items (and are thus equal). + } +} + +fun MathExpression.toComparableOperationList(): ComparableOperationList { + return ComparableOperationList.newBuilder().apply { + rootOperation = stripGroups().toComparableOperation().stabilizeNegation().sort() + }.build() +} + +private fun ComparableOperation.stabilizeNegation(): ComparableOperation { + return when (comparisonTypeCase) { + COMMUTATIVE_ACCUMULATION -> { + val stabilizedOperations = + commutativeAccumulation.combinedOperationsList.map { it.stabilizeNegation() } + when (commutativeAccumulation.accumulationType) { + AccumulationType.SUMMATION -> toBuilder().apply { + commutativeAccumulation = commutativeAccumulation.toBuilder().apply { + clearCombinedOperations() + addAllCombinedOperations(stabilizedOperations) + }.build() + }.build() + AccumulationType.PRODUCT -> { + // Negations can be combined for all constituent operations & brought up to the top-level + // operation. + val negativeCount = stabilizedOperations.count { it.isNegated } + if (isNegated) 1 else 0 + val positiveOperations = stabilizedOperations.map { it.positive() } + toBuilder().apply { + isNegated = (negativeCount % 2) == 1 + commutativeAccumulation = commutativeAccumulation.toBuilder().apply { + clearCombinedOperations() + addAllCombinedOperations(positiveOperations) + }.build() + }.build() + } + AccumulationType.ACCUMULATION_TYPE_UNSPECIFIED, AccumulationType.UNRECOGNIZED, null -> this + } + } + NON_COMMUTATIVE_OPERATION -> toBuilder().apply { + // Negation can't be extracted from commutative operations. + nonCommutativeOperation = when (nonCommutativeOperation.operationTypeCase) { + OperationTypeCase.DIVISION -> nonCommutativeOperation.toBuilder().apply { + division = nonCommutativeOperation.division.toBuilder().apply { + leftOperand = nonCommutativeOperation.division.leftOperand.stabilizeNegation() + rightOperand = nonCommutativeOperation.division.rightOperand.stabilizeNegation() + }.build() + }.build() + OperationTypeCase.EXPONENTIATION -> nonCommutativeOperation.toBuilder().apply { + exponentiation = nonCommutativeOperation.exponentiation.toBuilder().apply { + leftOperand = nonCommutativeOperation.exponentiation.leftOperand.stabilizeNegation() + rightOperand = nonCommutativeOperation.exponentiation.rightOperand.stabilizeNegation() + }.build() + }.build() + OperationTypeCase.SQUARE_ROOT -> nonCommutativeOperation.toBuilder().apply { + squareRoot = nonCommutativeOperation.squareRoot.stabilizeNegation() + }.build() + OperationTypeCase.OPERATIONTYPE_NOT_SET, null -> nonCommutativeOperation + } + }.build() + CONSTANT_TERM -> this + VARIABLE_TERM -> this + COMPARISONTYPE_NOT_SET, null -> this + } +} + +private fun ComparableOperation.sort(): ComparableOperation { + return when (comparisonTypeCase) { + COMMUTATIVE_ACCUMULATION -> toBuilder().apply { + commutativeAccumulation = commutativeAccumulation.toBuilder().apply { + clearCombinedOperations() + // Sort the operations themselves before sorting them relative to each other. + val innerSortedList = commutativeAccumulation.combinedOperationsList.map { it.sort() } + addAllCombinedOperations(innerSortedList.sortedWith(COMPARABLE_OPERATION_COMPARATOR)) + }.build() + }.build() + NON_COMMUTATIVE_OPERATION -> toBuilder().apply { + nonCommutativeOperation = when (nonCommutativeOperation.operationTypeCase) { + OperationTypeCase.DIVISION -> nonCommutativeOperation.toBuilder().apply { + division = nonCommutativeOperation.division.toBuilder().apply { + leftOperand = nonCommutativeOperation.division.leftOperand.sort() + rightOperand = nonCommutativeOperation.division.rightOperand.sort() + }.build() + }.build() + OperationTypeCase.EXPONENTIATION -> nonCommutativeOperation.toBuilder().apply { + exponentiation = nonCommutativeOperation.exponentiation.toBuilder().apply { + leftOperand = nonCommutativeOperation.exponentiation.leftOperand.sort() + rightOperand = nonCommutativeOperation.exponentiation.rightOperand.sort() + }.build() + }.build() + OperationTypeCase.SQUARE_ROOT -> nonCommutativeOperation.toBuilder().apply { + squareRoot = nonCommutativeOperation.squareRoot.sort() + }.build() + OperationTypeCase.OPERATIONTYPE_NOT_SET, null -> nonCommutativeOperation + } + }.build() + CONSTANT_TERM, VARIABLE_TERM, COMPARISONTYPE_NOT_SET, null -> this + } +} + +private fun MathExpression.stripGroups(): MathExpression { + return when (expressionTypeCase) { + BINARY_OPERATION -> toBuilder().apply { + binaryOperation = binaryOperation.toBuilder().apply { + leftOperand = binaryOperation.leftOperand.stripGroups() + rightOperand = binaryOperation.rightOperand.stripGroups() + }.build() + }.build() + UNARY_OPERATION -> toBuilder().apply { + unaryOperation = unaryOperation.toBuilder().apply { + operand = unaryOperation.operand.stripGroups() + }.build() + }.build() + FUNCTION_CALL -> toBuilder().apply { + functionCall = functionCall.toBuilder().apply { + argument = functionCall.argument.stripGroups() + }.build() + }.build() + GROUP -> group.stripGroups() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> this + } +} + +private fun MathExpression.toComparableOperation(): ComparableOperation { + return when (expressionTypeCase) { + CONSTANT -> ComparableOperation.newBuilder().apply { + constantTerm = constant + }.build() + VARIABLE -> ComparableOperation.newBuilder().apply { + variableTerm = variable + }.build() + BINARY_OPERATION -> when (binaryOperation.operator) { + BinaryOperator.ADD -> toSummation(isRhsNegative = false) + BinaryOperator.SUBTRACT -> toSummation(isRhsNegative = true) + BinaryOperator.MULTIPLY -> ComparableOperation.newBuilder().apply { + commutativeAccumulation = CommutativeAccumulation.newBuilder().apply { + accumulationType = AccumulationType.PRODUCT + addOperationToProduct(binaryOperation.leftOperand) + addOperationToProduct(binaryOperation.rightOperand) + }.build() + }.build() + BinaryOperator.DIVIDE -> + toNonCommutativeOperation(NonCommutativeOperation.Builder::setDivision) + BinaryOperator.EXPONENTIATE -> + toNonCommutativeOperation(NonCommutativeOperation.Builder::setExponentiation) + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> + ComparableOperation.getDefaultInstance() + } + UNARY_OPERATION -> when (unaryOperation.operator) { + UnaryOperator.NEGATE -> unaryOperation.operand.toComparableOperation().negate() + UnaryOperator.POSITIVE -> unaryOperation.operand.toComparableOperation() + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> + ComparableOperation.getDefaultInstance() + } + FUNCTION_CALL -> when (functionCall.functionType) { + FunctionType.SQUARE_ROOT -> ComparableOperation.newBuilder().apply { + nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { + squareRoot = functionCall.argument.toComparableOperation() + }.build() + }.build() + FunctionType.FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> + ComparableOperation.getDefaultInstance() + } + GROUP -> group.toComparableOperation() + EXPRESSIONTYPE_NOT_SET, null -> ComparableOperation.getDefaultInstance() + } +} + +private fun MathExpression.toSummation(isRhsNegative: Boolean): ComparableOperation { + return ComparableOperation.newBuilder().apply { + commutativeAccumulation = CommutativeAccumulation.newBuilder().apply { + accumulationType = AccumulationType.SUMMATION + addOperationToSum(binaryOperation.leftOperand, forceNegative = false) + addOperationToSum(binaryOperation.rightOperand, forceNegative = isRhsNegative) + }.build() + }.build() +} + +private fun CommutativeAccumulation.Builder.addOperationToSum( + expression: MathExpression, + forceNegative: Boolean +) { + // If the whole operation is negative, carry it to the left-hand side of the operation. + when (expression.binaryOperation.operator) { + BinaryOperator.ADD -> { + addOperationToSum(expression.binaryOperation.leftOperand, forceNegative) + addOperationToSum(expression.binaryOperation.rightOperand, forceNegative = false) + } + BinaryOperator.SUBTRACT -> { + addOperationToSum(expression.binaryOperation.leftOperand, forceNegative) + addOperationToSum(expression.binaryOperation.rightOperand, forceNegative = true) + } + else -> if (forceNegative) { + addCombinedOperations(expression.toComparableOperation().negate()) + } else addCombinedOperations(expression.toComparableOperation()) + } +} + +private fun CommutativeAccumulation.Builder.addOperationToProduct(expression: MathExpression) { + // This implies a binary operation. + if (expression.binaryOperation.operator == BinaryOperator.MULTIPLY) { + addOperationToProduct(expression.binaryOperation.leftOperand) + addOperationToProduct(expression.binaryOperation.rightOperand) + } else addCombinedOperations(expression.toComparableOperation()) +} + +private fun MathExpression.toNonCommutativeOperation( + setOperation: NonCommutativeOperation.Builder.( + NonCommutativeOperation.BinaryOperation + ) -> NonCommutativeOperation.Builder +): ComparableOperation { + return ComparableOperation.newBuilder().apply { + nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { + setOperation( + NonCommutativeOperation.BinaryOperation.newBuilder().apply { + leftOperand = binaryOperation.leftOperand.toComparableOperation() + rightOperand = binaryOperation.rightOperand.toComparableOperation() + }.build() + ) + }.build() + }.build() +} + +private fun ComparableOperation.positive(): ComparableOperation = + toBuilder().apply { isNegated = false }.build() + +private fun ComparableOperation.negate(): ComparableOperation = + toBuilder().apply { isNegated = true }.build() + // TODO: move these to the UI layer & have them utilize non-translatable strings. private val numberFormat by lazy { NumberFormat.getNumberInstance(Locale.US) } private val singularOrdinalNames = mapOf( diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 3994fb4faa9..2b4874b56ab 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -49,6 +49,7 @@ oppia_android_test( deps = [ "//model/src/main/proto:math_java_proto_lite", "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 60923595bcb..1d83f1f5ccb 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -6,10 +6,10 @@ import com.google.common.truth.DoubleSubject import com.google.common.truth.FailureMetadata import com.google.common.truth.IntegerSubject import com.google.common.truth.StringSubject -import com.google.common.truth.Subject import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage +import com.google.common.truth.extensions.proto.LiteProtoSubject import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.Fraction @@ -25,6 +25,11 @@ import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.Real import org.robolectric.annotation.LooperMode import kotlin.math.sqrt +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.PRODUCT +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.OppiaLanguage @@ -311,7 +316,7 @@ class NumericExpressionParserTest { val expression1 = parseNumericExpressionWithoutOptionalErrors("1") assertThat(expression1).hasStructureThatMatches { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } assertThat(expression1).evaluatesToIntegerThat().isEqualTo(1) @@ -321,7 +326,7 @@ class NumericExpressionParserTest { val expression2 = parseNumericExpressionWithoutOptionalErrors(" 2 ") assertThat(expression2).hasStructureThatMatches { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } assertThat(expression2).evaluatesToIntegerThat().isEqualTo(2) @@ -329,7 +334,7 @@ class NumericExpressionParserTest { val expression3 = parseNumericExpressionWithoutOptionalErrors(" 2.5 ") assertThat(expression3).hasStructureThatMatches { constant { - withIrrationalValueThat().isWithin(1e-5).of(2.5) + withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) } } assertThat(expression3).evaluatesToIrrationalThat().isWithin(1e-5).of(2.5) @@ -343,19 +348,19 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -372,12 +377,12 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -385,7 +390,7 @@ class NumericExpressionParserTest { } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -399,19 +404,19 @@ class NumericExpressionParserTest { division { leftOperand { constant { - withIntegerValueThat().isEqualTo(512) + withValueThat().isIntegerThat().isEqualTo(512) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(32) + withValueThat().isIntegerThat().isEqualTo(32) } } } } rightOperand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -423,7 +428,7 @@ class NumericExpressionParserTest { division { leftOperand { constant { - withIntegerValueThat().isEqualTo(512) + withValueThat().isIntegerThat().isEqualTo(512) } } rightOperand { @@ -431,12 +436,12 @@ class NumericExpressionParserTest { division { leftOperand { constant { - withIntegerValueThat().isEqualTo(32) + withValueThat().isIntegerThat().isEqualTo(32) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -451,7 +456,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -465,7 +470,7 @@ class NumericExpressionParserTest { val expression6 = parseNumericExpressionWithoutOptionalErrors("732") assertThat(expression6).hasStructureThatMatches { constant { - withIntegerValueThat().isEqualTo(732) + withValueThat().isIntegerThat().isEqualTo(732) } } assertThat(expression6).evaluatesToIntegerThat().isEqualTo(732) @@ -476,19 +481,19 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(7) + withValueThat().isIntegerThat().isEqualTo(7) } } } @@ -514,13 +519,13 @@ class NumericExpressionParserTest { leftOperand { // 3 constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { // 2 constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -528,7 +533,7 @@ class NumericExpressionParserTest { rightOperand { // 3 constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -548,13 +553,13 @@ class NumericExpressionParserTest { leftOperand { // 4 constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } rightOperand { // 7 constant { - withIntegerValueThat().isEqualTo(7) + withValueThat().isIntegerThat().isEqualTo(7) } } } @@ -562,7 +567,7 @@ class NumericExpressionParserTest { rightOperand { // 8 constant { - withIntegerValueThat().isEqualTo(8) + withValueThat().isIntegerThat().isEqualTo(8) } } } @@ -570,7 +575,7 @@ class NumericExpressionParserTest { rightOperand { // 3 constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -578,7 +583,7 @@ class NumericExpressionParserTest { rightOperand { // 2 constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -588,7 +593,7 @@ class NumericExpressionParserTest { rightOperand { // 7 constant { - withIntegerValueThat().isEqualTo(7) + withValueThat().isIntegerThat().isEqualTo(7) } } } @@ -609,12 +614,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -625,12 +630,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -648,7 +653,7 @@ class NumericExpressionParserTest { multiplication { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { @@ -656,12 +661,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -679,14 +684,14 @@ class NumericExpressionParserTest { multiplication { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -706,7 +711,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -716,12 +721,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -734,19 +739,19 @@ class NumericExpressionParserTest { subtraction { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(5) + withValueThat().isIntegerThat().isEqualTo(5) } } } @@ -767,7 +772,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -777,12 +782,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -795,19 +800,19 @@ class NumericExpressionParserTest { subtraction { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(5) + withValueThat().isIntegerThat().isEqualTo(5) } } } @@ -824,7 +829,7 @@ class NumericExpressionParserTest { group { group { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -838,7 +843,7 @@ class NumericExpressionParserTest { positive { operand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -854,7 +859,7 @@ class NumericExpressionParserTest { negation { operand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -868,14 +873,14 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { negation { operand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -889,14 +894,14 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { positive { operand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -910,14 +915,14 @@ class NumericExpressionParserTest { subtraction { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { negation { operand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -937,21 +942,21 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } } rightOperand { constant { - withIntegerValueThat().isEqualTo(7) + withValueThat().isIntegerThat().isEqualTo(7) } } } } rightOperand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -970,7 +975,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -979,7 +984,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -990,7 +995,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -1013,12 +1018,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1030,19 +1035,19 @@ class NumericExpressionParserTest { subtraction { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(7) + withValueThat().isIntegerThat().isEqualTo(7) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1058,14 +1063,14 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(5) + withValueThat().isIntegerThat().isEqualTo(5) } } rightOperand { negation { operand { constant { - withIntegerValueThat().isEqualTo(17) + withValueThat().isIntegerThat().isEqualTo(17) } } } @@ -1082,14 +1087,14 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { negation { operand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1111,14 +1116,14 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { negation { operand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1131,14 +1136,14 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { negation { operand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1155,21 +1160,21 @@ class NumericExpressionParserTest { subtraction { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -1189,12 +1194,12 @@ class NumericExpressionParserTest { division { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1204,12 +1209,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -1226,12 +1231,12 @@ class NumericExpressionParserTest { division { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1241,12 +1246,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -1271,14 +1276,14 @@ class NumericExpressionParserTest { leftOperand { group { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } rightOperand { group { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -1287,7 +1292,7 @@ class NumericExpressionParserTest { rightOperand { group { constant { - withIntegerValueThat().isEqualTo(5) + withValueThat().isIntegerThat().isEqualTo(5) } } } @@ -1300,13 +1305,13 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { group { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -1322,13 +1327,13 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { group { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -1337,7 +1342,7 @@ class NumericExpressionParserTest { rightOperand { group { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -1355,13 +1360,13 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { group { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -1371,12 +1376,12 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1393,13 +1398,13 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { group { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -1410,12 +1415,12 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1441,13 +1446,13 @@ class NumericExpressionParserTest { leftOperand { // 2 constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { // 3 constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -1456,7 +1461,7 @@ class NumericExpressionParserTest { // 4 group { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -1468,13 +1473,13 @@ class NumericExpressionParserTest { leftOperand { // 2 constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { // 3 constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -1499,12 +1504,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1525,7 +1530,7 @@ class NumericExpressionParserTest { multiplication { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { @@ -1533,12 +1538,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1559,12 +1564,12 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -1572,7 +1577,7 @@ class NumericExpressionParserTest { rightOperand { group { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -1591,14 +1596,14 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1606,7 +1611,7 @@ class NumericExpressionParserTest { rightOperand { group { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -1624,12 +1629,12 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1637,7 +1642,7 @@ class NumericExpressionParserTest { rightOperand { group { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -1658,14 +1663,14 @@ class NumericExpressionParserTest { multiplication { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { negation { operand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1681,13 +1686,13 @@ class NumericExpressionParserTest { multiplication { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { group { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1700,14 +1705,14 @@ class NumericExpressionParserTest { multiplication { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1721,14 +1726,14 @@ class NumericExpressionParserTest { multiplication { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1743,14 +1748,14 @@ class NumericExpressionParserTest { leftOperand { group { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } rightOperand { group { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1765,7 +1770,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1773,7 +1778,7 @@ class NumericExpressionParserTest { rightOperand { group { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1788,7 +1793,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1797,7 +1802,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1813,7 +1818,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1822,7 +1827,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1839,14 +1844,14 @@ class NumericExpressionParserTest { leftOperand { group { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } rightOperand { group { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1855,7 +1860,7 @@ class NumericExpressionParserTest { rightOperand { group { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1872,7 +1877,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1881,7 +1886,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1892,7 +1897,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1911,7 +1916,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1920,7 +1925,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1931,7 +1936,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1959,13 +1964,13 @@ class NumericExpressionParserTest { leftOperand { // 2 constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { // 2 constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1976,7 +1981,7 @@ class NumericExpressionParserTest { // 4 operand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -1989,13 +1994,13 @@ class NumericExpressionParserTest { leftOperand { // 7 constant { - withIntegerValueThat().isEqualTo(7) + withValueThat().isIntegerThat().isEqualTo(7) } } rightOperand { // 2 constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2009,7 +2014,7 @@ class NumericExpressionParserTest { division { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { @@ -2017,12 +2022,12 @@ class NumericExpressionParserTest { subtraction { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2038,7 +2043,7 @@ class NumericExpressionParserTest { leftOperand { group { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -2047,12 +2052,12 @@ class NumericExpressionParserTest { subtraction { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2067,7 +2072,7 @@ class NumericExpressionParserTest { division { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { @@ -2076,12 +2081,12 @@ class NumericExpressionParserTest { subtraction { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2105,7 +2110,7 @@ class NumericExpressionParserTest { val expression1 = parseAlgebraicExpressionWithoutOptionalErrors("1") assertThat(expression1).hasStructureThatMatches { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } assertThat(expression1).evaluatesToIntegerThat().isEqualTo(1) @@ -2120,7 +2125,7 @@ class NumericExpressionParserTest { val expression2 = parseAlgebraicExpressionWithoutOptionalErrors(" 2 ") assertThat(expression2).hasStructureThatMatches { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } assertThat(expression2).evaluatesToIntegerThat().isEqualTo(2) @@ -2128,7 +2133,7 @@ class NumericExpressionParserTest { val expression3 = parseAlgebraicExpressionWithoutOptionalErrors(" 2.5 ") assertThat(expression3).hasStructureThatMatches { constant { - withIrrationalValueThat().isWithin(1e-5).of(2.5) + withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) } } assertThat(expression3).evaluatesToIrrationalThat().isWithin(1e-5).of(2.5) @@ -2161,19 +2166,19 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2190,12 +2195,12 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -2203,7 +2208,7 @@ class NumericExpressionParserTest { } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2217,19 +2222,19 @@ class NumericExpressionParserTest { division { leftOperand { constant { - withIntegerValueThat().isEqualTo(512) + withValueThat().isIntegerThat().isEqualTo(512) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(32) + withValueThat().isIntegerThat().isEqualTo(32) } } } } rightOperand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -2241,7 +2246,7 @@ class NumericExpressionParserTest { division { leftOperand { constant { - withIntegerValueThat().isEqualTo(512) + withValueThat().isIntegerThat().isEqualTo(512) } } rightOperand { @@ -2249,12 +2254,12 @@ class NumericExpressionParserTest { division { leftOperand { constant { - withIntegerValueThat().isEqualTo(32) + withValueThat().isIntegerThat().isEqualTo(32) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -2269,7 +2274,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2307,7 +2312,7 @@ class NumericExpressionParserTest { rightOperand { group { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2317,7 +2322,7 @@ class NumericExpressionParserTest { val expression6 = parseAlgebraicExpressionWithoutOptionalErrors("732") assertThat(expression6).hasStructureThatMatches { constant { - withIntegerValueThat().isEqualTo(732) + withValueThat().isIntegerThat().isEqualTo(732) } } assertThat(expression6).evaluatesToIntegerThat().isEqualTo(732) @@ -2330,19 +2335,19 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(7) + withValueThat().isIntegerThat().isEqualTo(7) } } } @@ -2368,13 +2373,13 @@ class NumericExpressionParserTest { leftOperand { // 3 constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { // 2 constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2382,7 +2387,7 @@ class NumericExpressionParserTest { rightOperand { // 3 constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -2402,13 +2407,13 @@ class NumericExpressionParserTest { leftOperand { // 4 constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } rightOperand { // 7 constant { - withIntegerValueThat().isEqualTo(7) + withValueThat().isIntegerThat().isEqualTo(7) } } } @@ -2416,7 +2421,7 @@ class NumericExpressionParserTest { rightOperand { // 8 constant { - withIntegerValueThat().isEqualTo(8) + withValueThat().isIntegerThat().isEqualTo(8) } } } @@ -2424,7 +2429,7 @@ class NumericExpressionParserTest { rightOperand { // 3 constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -2432,7 +2437,7 @@ class NumericExpressionParserTest { rightOperand { // 2 constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2442,7 +2447,7 @@ class NumericExpressionParserTest { rightOperand { // 7 constant { - withIntegerValueThat().isEqualTo(7) + withValueThat().isIntegerThat().isEqualTo(7) } } } @@ -2463,12 +2468,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2479,12 +2484,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -2502,7 +2507,7 @@ class NumericExpressionParserTest { multiplication { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { @@ -2510,12 +2515,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2533,14 +2538,14 @@ class NumericExpressionParserTest { multiplication { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2561,7 +2566,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2578,7 +2583,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2588,12 +2593,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2606,19 +2611,19 @@ class NumericExpressionParserTest { subtraction { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(5) + withValueThat().isIntegerThat().isEqualTo(5) } } } @@ -2639,7 +2644,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2649,12 +2654,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2667,19 +2672,19 @@ class NumericExpressionParserTest { subtraction { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(5) + withValueThat().isIntegerThat().isEqualTo(5) } } } @@ -2696,7 +2701,7 @@ class NumericExpressionParserTest { group { group { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -2710,7 +2715,7 @@ class NumericExpressionParserTest { positive { operand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -2726,7 +2731,7 @@ class NumericExpressionParserTest { negation { operand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -2740,14 +2745,14 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { negation { operand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -2761,14 +2766,14 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { positive { operand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -2782,14 +2787,14 @@ class NumericExpressionParserTest { subtraction { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { negation { operand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -2809,21 +2814,21 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } } rightOperand { constant { - withIntegerValueThat().isEqualTo(7) + withValueThat().isIntegerThat().isEqualTo(7) } } } } rightOperand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -2842,7 +2847,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2851,7 +2856,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -2862,7 +2867,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -2885,12 +2890,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2902,19 +2907,19 @@ class NumericExpressionParserTest { subtraction { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(7) + withValueThat().isIntegerThat().isEqualTo(7) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2930,14 +2935,14 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(5) + withValueThat().isIntegerThat().isEqualTo(5) } } rightOperand { negation { operand { constant { - withIntegerValueThat().isEqualTo(17) + withValueThat().isIntegerThat().isEqualTo(17) } } } @@ -2954,14 +2959,14 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { negation { operand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -2983,14 +2988,14 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { negation { operand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3003,14 +3008,14 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { negation { operand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3027,21 +3032,21 @@ class NumericExpressionParserTest { subtraction { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -3061,12 +3066,12 @@ class NumericExpressionParserTest { division { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3076,12 +3081,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -3098,12 +3103,12 @@ class NumericExpressionParserTest { division { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3113,12 +3118,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -3143,14 +3148,14 @@ class NumericExpressionParserTest { leftOperand { group { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } rightOperand { group { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -3159,7 +3164,7 @@ class NumericExpressionParserTest { rightOperand { group { constant { - withIntegerValueThat().isEqualTo(5) + withValueThat().isIntegerThat().isEqualTo(5) } } } @@ -3172,13 +3177,13 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { group { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -3194,13 +3199,13 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { group { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -3209,7 +3214,7 @@ class NumericExpressionParserTest { rightOperand { group { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -3227,13 +3232,13 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { group { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -3243,12 +3248,12 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3265,13 +3270,13 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { group { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -3282,12 +3287,12 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3313,13 +3318,13 @@ class NumericExpressionParserTest { leftOperand { // 2 constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { // 3 constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -3328,7 +3333,7 @@ class NumericExpressionParserTest { // 4 group { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -3340,13 +3345,13 @@ class NumericExpressionParserTest { leftOperand { // 2 constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { // 3 constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -3371,12 +3376,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3394,7 +3399,7 @@ class NumericExpressionParserTest { multiplication { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { @@ -3415,7 +3420,7 @@ class NumericExpressionParserTest { multiplication { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { @@ -3423,12 +3428,12 @@ class NumericExpressionParserTest { addition { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3449,12 +3454,12 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -3462,7 +3467,7 @@ class NumericExpressionParserTest { rightOperand { group { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -3481,14 +3486,14 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3496,7 +3501,7 @@ class NumericExpressionParserTest { rightOperand { group { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -3514,12 +3519,12 @@ class NumericExpressionParserTest { exponentiation { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3527,7 +3532,7 @@ class NumericExpressionParserTest { rightOperand { group { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -3548,14 +3553,14 @@ class NumericExpressionParserTest { multiplication { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { negation { operand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3571,13 +3576,13 @@ class NumericExpressionParserTest { multiplication { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { group { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3590,14 +3595,14 @@ class NumericExpressionParserTest { multiplication { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3611,14 +3616,14 @@ class NumericExpressionParserTest { multiplication { leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3633,14 +3638,14 @@ class NumericExpressionParserTest { leftOperand { group { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } rightOperand { group { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3655,7 +3660,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3663,7 +3668,7 @@ class NumericExpressionParserTest { rightOperand { group { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3678,7 +3683,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3687,7 +3692,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3703,7 +3708,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3712,7 +3717,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3729,14 +3734,14 @@ class NumericExpressionParserTest { leftOperand { group { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } rightOperand { group { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3745,7 +3750,7 @@ class NumericExpressionParserTest { rightOperand { group { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3762,7 +3767,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3771,7 +3776,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3782,7 +3787,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3801,7 +3806,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3810,7 +3815,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3821,7 +3826,7 @@ class NumericExpressionParserTest { functionCallTo(SQUARE_ROOT) { argument { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3844,7 +3849,7 @@ class NumericExpressionParserTest { // 2 leftOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } // x^2 @@ -3859,7 +3864,7 @@ class NumericExpressionParserTest { // 2 rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3881,7 +3886,7 @@ class NumericExpressionParserTest { // 3 operand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -3904,13 +3909,13 @@ class NumericExpressionParserTest { leftOperand { // 2 constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { // 2 constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3921,7 +3926,7 @@ class NumericExpressionParserTest { // 4 operand { constant { - withIntegerValueThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -3934,13 +3939,13 @@ class NumericExpressionParserTest { leftOperand { // 7 constant { - withIntegerValueThat().isEqualTo(7) + withValueThat().isIntegerThat().isEqualTo(7) } } rightOperand { // 2 constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3954,7 +3959,7 @@ class NumericExpressionParserTest { division { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { @@ -3962,12 +3967,12 @@ class NumericExpressionParserTest { subtraction { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -3983,7 +3988,7 @@ class NumericExpressionParserTest { leftOperand { group { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -3992,12 +3997,12 @@ class NumericExpressionParserTest { subtraction { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -4012,7 +4017,7 @@ class NumericExpressionParserTest { division { leftOperand { constant { - withIntegerValueThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { @@ -4021,12 +4026,12 @@ class NumericExpressionParserTest { subtraction { leftOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -4103,7 +4108,7 @@ class NumericExpressionParserTest { } rightOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } } @@ -4111,7 +4116,7 @@ class NumericExpressionParserTest { } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -4135,7 +4140,7 @@ class NumericExpressionParserTest { } rightOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } } @@ -4151,7 +4156,7 @@ class NumericExpressionParserTest { } rightOperand { constant { - withIntegerValueThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(1) } } } @@ -4185,7 +4190,7 @@ class NumericExpressionParserTest { } rightOperand { constant { - withIntegerValueThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -4217,7 +4222,7 @@ class NumericExpressionParserTest { } assertThat(equation5).hasRightHandSideThat().hasStructureThatMatches { constant { - withIntegerValueThat().isEqualTo(0) + withValueThat().isIntegerThat().isEqualTo(0) } } } @@ -4691,17 +4696,1440 @@ class NumericExpressionParserTest { ) } - @DslMarker - private annotation class ExpressionComparatorMarker + @Test + fun testToComparableOperation() { + // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). + + val exp1 = parseNumericExpressionWithAllErrors("1") + assertThat(exp1.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + + val exp2 = parseNumericExpressionWithAllErrors("-1") + assertThat(exp2.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + + val exp3 = parseNumericExpressionWithAllErrors("1+3+4") + assertThat(exp3.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp4 = parseNumericExpressionWithAllErrors("-1-2-3") + assertThat(exp4.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp5 = parseNumericExpressionWithAllErrors("1+2-3") + assertThat(exp5.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp6 = parseNumericExpressionWithAllErrors("2*3*4") + assertThat(exp6.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp7 = parseNumericExpressionWithAllErrors("1-2*3") + assertThat(exp7.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isTrue() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + + val exp8 = parseNumericExpressionWithAllErrors("2*3-4") + assertThat(exp8.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp9 = parseNumericExpressionWithAllErrors("1+2*3-4+8*7*6-9") + assertThat(exp9.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(3) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(9) + } + } + } + } + + val exp10 = parseNumericExpressionWithAllErrors("2/3/4") + assertThat(exp10.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + division { + leftOperand { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + division { + leftOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + val exp11 = parseNumericExpressionWithoutOptionalErrors("2^3^4") + assertThat(exp11.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + val exp12 = parseNumericExpressionWithAllErrors("1+2/3+3") + assertThat(exp12.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + division { + leftOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp13 = parseNumericExpressionWithAllErrors("1+(2/3)+3") + assertThat(exp13.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + division { + leftOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp14 = parseNumericExpressionWithAllErrors("1+2^3+3") + assertThat(exp14.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp15 = parseNumericExpressionWithAllErrors("1+(2^3)+3") + assertThat(exp15.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + // 2*3/4*7 is the same as ((2*3)/4)*7 due to precedence and associativity, so there's not much + // reordering possible. + val exp16 = parseNumericExpressionWithAllErrors("2*3/4*7") + assertThat(exp16.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + division { + leftOperand { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + + val exp17 = parseNumericExpressionWithAllErrors("2*(3/4)*7") + assertThat(exp17.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + division { + leftOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + + val exp18 = parseNumericExpressionWithAllErrors("-3*sqrt(2)") + assertThat(exp18.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + squareRootWithArgument { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp19 = parseNumericExpressionWithAllErrors("1+(2+(3+(4+5)))") + assertThat(exp19.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + + val exp20 = parseNumericExpressionWithAllErrors("2*(3*(4*(5*6)))") + assertThat(exp20.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + } + } + + val exp21 = parseAlgebraicExpressionWithAllErrors("x") + assertThat(exp21.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + + val exp22 = parseAlgebraicExpressionWithAllErrors("-x") + assertThat(exp22.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("x") + } + } + + val exp23 = parseAlgebraicExpressionWithAllErrors("1+x+y") + assertThat(exp23.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp24 = parseAlgebraicExpressionWithAllErrors("-1-x-y") + assertThat(exp24.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp25 = parseAlgebraicExpressionWithAllErrors("1+x-y") + assertThat(exp25.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp26 = parseAlgebraicExpressionWithAllErrors("2xy") + assertThat(exp26.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp27 = parseAlgebraicExpressionWithAllErrors("1-xy") + assertThat(exp27.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isTrue() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + + val exp28 = parseAlgebraicExpressionWithAllErrors("xy-4") + assertThat(exp28.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp29 = parseAlgebraicExpressionWithAllErrors("1+xy-4+yz-9") + assertThat(exp29.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(3) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(9) + } + } + } + } + + val exp30 = parseAlgebraicExpressionWithAllErrors("2/x/y") + assertThat(exp30.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + division { + leftOperand { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + division { + leftOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + } + + val exp31 = parseAlgebraicExpressionWithoutOptionalErrors("x^3^4") + assertThat(exp31.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + val exp32 = parseAlgebraicExpressionWithAllErrors("1+x/y+z") + assertThat(exp32.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + division { + leftOperand { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + val exp33 = parseAlgebraicExpressionWithAllErrors("1+(x/y)+z") + assertThat(exp33.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + division { + leftOperand { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + val exp34 = parseAlgebraicExpressionWithAllErrors("1+x^3+y") + assertThat(exp34.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp35 = parseAlgebraicExpressionWithAllErrors("1+(x^3)+y") + assertThat(exp35.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + // 2*3/4*7 is the same as ((2*3)/4)*7 due to precedence and associativity, so there's not much + // reordering possible. + val exp36 = parseAlgebraicExpressionWithAllErrors("2*x/y*z") + assertThat(exp36.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + division { + leftOperand { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + val exp37 = parseAlgebraicExpressionWithAllErrors("2*(x/y)*z") + assertThat(exp37.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + division { + leftOperand { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + val exp38 = parseAlgebraicExpressionWithAllErrors("-2*sqrt(x)") + assertThat(exp38.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + nonCommutativeOperation { + squareRootWithArgument { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + val exp39 = parseAlgebraicExpressionWithAllErrors("1+(x+(3+(z+y)))") + assertThat(exp39.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + val exp40 = parseAlgebraicExpressionWithAllErrors("2*(x*(4*(zy)))") + assertThat(exp40.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + // Equality tests: + val list1 = createComparableOperationListFromNumericExpression("(1+2)+3") + val list2 = createComparableOperationListFromNumericExpression("1+(2+3)") + assertThat(list1).isEqualTo(list2) + + val list3 = createComparableOperationListFromNumericExpression("1+2+3") + val list4 = createComparableOperationListFromNumericExpression("3+2+1") + assertThat(list3).isEqualTo(list4) + + val list5 = createComparableOperationListFromNumericExpression("1-2-3") + val list6 = createComparableOperationListFromNumericExpression("-3 + -2 + 1") + assertThat(list5).isEqualTo(list6) + + val list7 = createComparableOperationListFromNumericExpression("1-2-3") + val list8 = createComparableOperationListFromNumericExpression("-3-2+1") + assertThat(list7).isEqualTo(list8) + + val list9 = createComparableOperationListFromNumericExpression("1-2-3") + val list10 = createComparableOperationListFromNumericExpression("-3-2+1") + assertThat(list9).isEqualTo(list10) + + val list11 = createComparableOperationListFromNumericExpression("1-2-3") + val list12 = createComparableOperationListFromNumericExpression("3-2-1") + assertThat(list11).isNotEqualTo(list12) + + val list13 = createComparableOperationListFromNumericExpression("2*3*4") + val list14 = createComparableOperationListFromNumericExpression("4*3*2") + assertThat(list13).isEqualTo(list14) + + val list15 = createComparableOperationListFromNumericExpression("2*(3/4)") + val list16 = createComparableOperationListFromNumericExpression("3/4*2") + assertThat(list15).isEqualTo(list16) + + val list17 = createComparableOperationListFromNumericExpression("2*3/4") + val list18 = createComparableOperationListFromNumericExpression("3/4*2") + assertThat(list17).isNotEqualTo(list18) + + val list19 = createComparableOperationListFromNumericExpression("2*3/4") + val list20 = createComparableOperationListFromNumericExpression("2*4/3") + assertThat(list19).isNotEqualTo(list20) + + val list21 = createComparableOperationListFromNumericExpression("2*3/4*7") + val list22 = createComparableOperationListFromNumericExpression("3/4*7*2") + assertThat(list21).isNotEqualTo(list22) + + val list23 = createComparableOperationListFromNumericExpression("2*3/4*7") + val list24 = createComparableOperationListFromNumericExpression("7*(3*2/4)") + assertThat(list23).isEqualTo(list24) + + val list25 = createComparableOperationListFromNumericExpression("2*3/4*7") + val list26 = createComparableOperationListFromNumericExpression("7*3*2/4") + assertThat(list25).isNotEqualTo(list26) + + val list27 = createComparableOperationListFromNumericExpression("-2*3") + val list28 = createComparableOperationListFromNumericExpression("3*-2") + assertThat(list27).isEqualTo(list28) + + val list29 = createComparableOperationListFromNumericExpression("2^3") + val list30 = createComparableOperationListFromNumericExpression("3^2") + assertThat(list29).isNotEqualTo(list30) + + val list31 = createComparableOperationListFromNumericExpression("-(1+2)") + val list32 = createComparableOperationListFromNumericExpression("-1+2") + assertThat(list31).isNotEqualTo(list32) + + val list33 = createComparableOperationListFromNumericExpression("-(1+2)") + val list34 = createComparableOperationListFromNumericExpression("-1-2") + assertThat(list33).isNotEqualTo(list34) + + val list35 = createComparableOperationListFromAlgebraicExpression("x(x+1)") + val list36 = createComparableOperationListFromAlgebraicExpression("(1+x)x") + assertThat(list35).isEqualTo(list36) + + val list37 = createComparableOperationListFromAlgebraicExpression("x(x+1)") + val list38 = createComparableOperationListFromAlgebraicExpression("x^2+x") + assertThat(list37).isNotEqualTo(list38) + + val list39 = createComparableOperationListFromAlgebraicExpression("x^2*sqrt(x)") + val list40 = createComparableOperationListFromAlgebraicExpression("x") + assertThat(list39).isNotEqualTo(list40) + + val list41 = createComparableOperationListFromAlgebraicExpression("xyz") + val list42 = createComparableOperationListFromAlgebraicExpression("zyx") + assertThat(list41).isEqualTo(list42) + + val list43 = createComparableOperationListFromAlgebraicExpression("1+xy-2") + val list44 = createComparableOperationListFromAlgebraicExpression("-2+1+yx") + assertThat(list43).isEqualTo(list44) + + // TODO: add tests for comparator/sorting & negation simplification? + } + + private fun createComparableOperationListFromNumericExpression(expression: String) = + parseNumericExpressionWithAllErrors(expression).toComparableOperationList() + + private fun createComparableOperationListFromAlgebraicExpression(expression: String) = + parseAlgebraicExpressionWithAllErrors(expression).toComparableOperationList() + + @DslMarker private annotation class ExpressionComparatorMarker + + @DslMarker private annotation class ComparableOperationComparatorMarker // See: https://kotlinlang.org/docs/type-safe-builders.html. private class MathExpressionSubject( metadata: FailureMetadata, private val actual: MathExpression - ) : Subject(metadata, actual) { - fun hasStructureThatMatches(init: ExpressionComparator.() -> Unit): ExpressionComparator { + ) : LiteProtoSubject(metadata, actual) { + fun hasStructureThatMatches(init: ExpressionComparator.() -> Unit) { // TODO: maybe verify that all aspects are verified? - return ExpressionComparator.createFromExpression(actual).also(init) + ExpressionComparator.createFromExpression(actual).also(init) } fun evaluatesToRationalThat(): FractionSubject = @@ -4814,15 +6242,7 @@ class NumericExpressionParserTest { @ExpressionComparatorMarker class ConstantComparator private constructor(private val constant: Real) { - fun withIntegerValueThat(): IntegerSubject { - assertThat(constant.realTypeCase).isEqualTo(Real.RealTypeCase.INTEGER) - return assertThat(constant.integer) - } - - fun withIrrationalValueThat(): DoubleSubject { - assertThat(constant.realTypeCase).isEqualTo(Real.RealTypeCase.IRRATIONAL) - return assertThat(constant.irrational) - } + fun withValueThat(): RealSubject = assertThat(constant) internal companion object { fun createFromExpression(expression: MathExpression): ConstantComparator { @@ -4914,7 +6334,7 @@ class NumericExpressionParserTest { private class MathEquationSubject( metadata: FailureMetadata, private val actual: MathEquation - ) : Subject(metadata, actual) { + ) : LiteProtoSubject(metadata, actual) { fun hasLeftHandSideThat(): MathExpressionSubject = assertThat(actual.leftSide) fun hasRightHandSideThat(): MathExpressionSubject = assertThat(actual.rightSide) @@ -4956,11 +6376,11 @@ class NumericExpressionParserTest { } } - // TODO: move this to a common location. + // TODO: move these to a common location. private class FractionSubject( metadata: FailureMetadata, private val actual: Fraction - ) : Subject(metadata, actual) { + ) : LiteProtoSubject(metadata, actual) { fun hasNegativePropertyThat(): BooleanSubject = assertThat(actual.isNegative) fun hasWholeNumberThat(): IntegerSubject = assertThat(actual.wholeNumber) @@ -4972,6 +6392,185 @@ class NumericExpressionParserTest { fun evaluatesToRealThat(): DoubleSubject = assertThat(actual.toDouble()) } + private class RealSubject( + metadata: FailureMetadata, + private val actual: Real + ) : LiteProtoSubject(metadata, actual) { + fun isRationalThat(): FractionSubject { + verifyTypeToBe(Real.RealTypeCase.RATIONAL) + return assertThat(actual.rational) + } + + fun isIrrationalThat(): DoubleSubject { + verifyTypeToBe(Real.RealTypeCase.IRRATIONAL) + return assertThat(actual.irrational) + } + + fun isIntegerThat(): IntegerSubject { + verifyTypeToBe(Real.RealTypeCase.INTEGER) + return assertThat(actual.integer) + } + + private fun verifyTypeToBe(expected: Real.RealTypeCase) { + assertWithMessage("Expected real type to be $expected, not: ${actual.realTypeCase}") + .that(actual.realTypeCase) + .isEqualTo(expected) + } + } + + private class ComparableOperationListSubject( + metadata: FailureMetadata, + private val actual: ComparableOperationList + ) : LiteProtoSubject(metadata, actual) { + fun hasStructureThatMatches(init: ComparableOperationComparator.() -> Unit) { + ComparableOperationComparator.createFrom(actual.rootOperation).also(init) + } + + @ComparableOperationComparatorMarker + class ComparableOperationComparator private constructor( + private val operation: ComparableOperation + ) { + fun hasNegatedPropertyThat(): BooleanSubject = assertThat(operation.isNegated) + + fun commutativeAccumulationWithType( + type: ComparableOperationList.CommutativeAccumulation.AccumulationType, + init: CommutativeAccumulationComparator.() -> Unit + ): CommutativeAccumulationComparator = + CommutativeAccumulationComparator.createFrom(type, operation).also(init) + + fun nonCommutativeOperation( + init: NonCommutativeOperationComparator.() -> Unit + ): NonCommutativeOperationComparator = + NonCommutativeOperationComparator.createFrom(operation).also(init) + + fun constantTerm(init: ConstantTermComparator.() -> Unit): ConstantTermComparator = + ConstantTermComparator.createFrom(operation).also(init) + + fun variableTerm(init: VariableTermComparator.() -> Unit): VariableTermComparator = + VariableTermComparator.createFrom(operation).also(init) + + internal companion object { + fun createFrom(operation: ComparableOperation): ComparableOperationComparator = + ComparableOperationComparator(operation) + } + } + + @ComparableOperationComparatorMarker + class CommutativeAccumulationComparator private constructor( + private val accumulation: ComparableOperationList.CommutativeAccumulation + ) { + fun hasOperandCountThat(): IntegerSubject = assertThat(accumulation.combinedOperationsCount) + + fun index( + index: Int, + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator { + return ComparableOperationComparator.createFrom( + accumulation.combinedOperationsList[index] + ).also(init) + } + + internal companion object { + fun createFrom( + type: ComparableOperationList.CommutativeAccumulation.AccumulationType, + operation: ComparableOperation + ): CommutativeAccumulationComparator { + assertThat(operation.comparisonTypeCase) + .isEqualTo(ComparisonTypeCase.COMMUTATIVE_ACCUMULATION) + assertThat(operation.commutativeAccumulation.accumulationType).isEqualTo(type) + return CommutativeAccumulationComparator(operation.commutativeAccumulation) + } + } + } + + @ComparableOperationComparatorMarker + class NonCommutativeOperationComparator private constructor( + private val operation: ComparableOperationList.NonCommutativeOperation + ) { + fun division(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + verifyTypeAs(ComparableOperationList.NonCommutativeOperation.OperationTypeCase.DIVISION) + return BinaryOperationComparator.createFrom(operation.division).also(init) + } + + fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + verifyTypeAs( + ComparableOperationList.NonCommutativeOperation.OperationTypeCase.EXPONENTIATION + ) + return BinaryOperationComparator.createFrom(operation.exponentiation).also(init) + } + + fun squareRootWithArgument( + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator { + verifyTypeAs(ComparableOperationList.NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT) + return ComparableOperationComparator.createFrom(operation.squareRoot).also(init) + } + + private fun verifyTypeAs( + type: ComparableOperationList.NonCommutativeOperation.OperationTypeCase + ) { + assertThat(operation.operationTypeCase).isEqualTo(type) + } + + internal companion object { + fun createFrom(operation: ComparableOperation): NonCommutativeOperationComparator { + assertThat(operation.comparisonTypeCase) + .isEqualTo(ComparisonTypeCase.NON_COMMUTATIVE_OPERATION) + return NonCommutativeOperationComparator(operation.nonCommutativeOperation) + } + } + } + + @ComparableOperationComparatorMarker + class BinaryOperationComparator private constructor( + private val operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation + ) { + fun leftOperand( + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator = + ComparableOperationComparator.createFrom(operation.leftOperand).also(init) + + fun rightOperand( + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator = + ComparableOperationComparator.createFrom(operation.rightOperand).also(init) + + internal companion object { + fun createFrom( + operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation + ): BinaryOperationComparator = BinaryOperationComparator(operation) + } + } + + @ComparableOperationComparatorMarker + class ConstantTermComparator private constructor( + private val constant: Real + ) { + fun withValueThat(): RealSubject = assertThat(constant) + + internal companion object { + fun createFrom(operation: ComparableOperation): ConstantTermComparator { + assertThat(operation.comparisonTypeCase).isEqualTo(ComparisonTypeCase.CONSTANT_TERM) + return ConstantTermComparator(operation.constantTerm) + } + } + } + + @ComparableOperationComparatorMarker + class VariableTermComparator private constructor( + private val variableName: String + ) { + fun withNameThat(): StringSubject = assertThat(variableName) + + internal companion object { + fun createFrom(operation: ComparableOperation): VariableTermComparator { + assertThat(operation.comparisonTypeCase).isEqualTo(ComparisonTypeCase.VARIABLE_TERM) + return VariableTermComparator(operation.variableTerm) + } + } + } + } + private companion object { // TODO: fix helper API. @@ -5060,5 +6659,10 @@ class NumericExpressionParserTest { private fun assertThat(actual: Fraction): FractionSubject = assertAbout(::FractionSubject).that(actual) + + private fun assertThat(actual: Real): RealSubject = assertAbout(::RealSubject).that(actual) + + private fun assertThat(actual: ComparableOperationList): ComparableOperationListSubject = + assertAbout(::ComparableOperationListSubject).that(actual) } } From c8cd9f4c5e990b4d37d6acd977447558e3c48d9c Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 9 Dec 2021 13:37:50 -0800 Subject: [PATCH 087/289] Remove mult/div associativity. This updates division to be treated as a multiplicative inverse so that expressions like 2*3/4*7 and 7*3/4*2 can be considered equivalent. --- model/src/main/proto/math.proto | 30 +- .../util/math/MathExpressionExtensions.kt | 78 +-- .../util/math/NumericExpressionParserTest.kt | 534 +++++++++++------- 3 files changed, 396 insertions(+), 246 deletions(-) diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 68daa1a8cbc..803fdff16b8 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -90,17 +90,34 @@ message Real { } } +// Represents a list of comparable mathematics operations. 'Comparable' here means that this +// structure provides a trivial way to compare commutative operations (i.e. by extracting terms from +// multiple subsequent commutative operations into lists that can be deterministically sorted). This +// structure is meant to provide a means to compare two expressions without considering +// associativity or commutativity (though the latter requires the operation lists stored within this +// structure to be sorted before using standard proto equals checking). message ComparableOperationList { message ComparableOperation { + // Treat this operation (e.g. x) as negated (e.g. -x). bool is_negated = 1; + // Treat this operation (e.g. x) as a multiplicative inverse (e.g. 1/x). + bool is_inverted = 2; + oneof comparison_type { - CommutativeAccumulation commutative_accumulation = 2; - NonCommutativeOperation non_commutative_operation = 3; - Real constant_term = 4; - string variable_term = 5; + CommutativeAccumulation commutative_accumulation = 3; + NonCommutativeOperation non_commutative_operation = 4; + Real constant_term = 5; + string variable_term = 6; } } + // Represents an accumulation of operations (such as a summation or product). This helps simplify + // comparison across commutative boundaries by collecting terms into sortable lists, such as the + // expression 1+2+3 becoming [1,2,3] and trivially comparable to [3,2,1] from 3+2+1. + // + // Subsequent subtractions are treated as additions with each term arithmetically negated (i.e. + // f(x)=-x). Similarly, divisions are considered multiplications with each divisor being + // multiplicatively inverted (i.e. the reciprocal function: f(x)=1/x). message CommutativeAccumulation { enum AccumulationType { ACCUMULATION_TYPE_UNSPECIFIED = 0; @@ -113,9 +130,8 @@ message ComparableOperationList { } message NonCommutativeOperation { oneof operation_type { - BinaryOperation division = 1; - BinaryOperation exponentiation = 2; - ComparableOperation square_root = 3; + BinaryOperation exponentiation = 1; + ComparableOperation square_root = 2; } message BinaryOperation { diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index f80184ecedf..808ba59c5f3 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -70,6 +70,7 @@ private val COMPARABLE_OPERATION_COMPARATOR: Comparator by // isn't valid until it's fully assembled). Comparator.comparing(ComparableOperation::getComparisonTypeCase) .thenComparing(ComparableOperation::getIsNegated) + .thenComparing(ComparableOperation::getIsInverted) .selectAmong( ComparableOperation::getComparisonTypeCase, COMMUTATIVE_ACCUMULATION to comparingDeferred( @@ -102,9 +103,6 @@ private val NON_COMMUTATIVE_OPERATION_COMPARATOR: Comparator toBuilder().apply { // Negation can't be extracted from commutative operations. nonCommutativeOperation = when (nonCommutativeOperation.operationTypeCase) { - OperationTypeCase.DIVISION -> nonCommutativeOperation.toBuilder().apply { - division = nonCommutativeOperation.division.toBuilder().apply { - leftOperand = nonCommutativeOperation.division.leftOperand.stabilizeNegation() - rightOperand = nonCommutativeOperation.division.rightOperand.stabilizeNegation() - }.build() - }.build() OperationTypeCase.EXPONENTIATION -> nonCommutativeOperation.toBuilder().apply { exponentiation = nonCommutativeOperation.exponentiation.toBuilder().apply { leftOperand = nonCommutativeOperation.exponentiation.leftOperand.stabilizeNegation() @@ -235,12 +227,6 @@ private fun ComparableOperation.sort(): ComparableOperation { }.build() NON_COMMUTATIVE_OPERATION -> toBuilder().apply { nonCommutativeOperation = when (nonCommutativeOperation.operationTypeCase) { - OperationTypeCase.DIVISION -> nonCommutativeOperation.toBuilder().apply { - division = nonCommutativeOperation.division.toBuilder().apply { - leftOperand = nonCommutativeOperation.division.leftOperand.sort() - rightOperand = nonCommutativeOperation.division.rightOperand.sort() - }.build() - }.build() OperationTypeCase.EXPONENTIATION -> nonCommutativeOperation.toBuilder().apply { exponentiation = nonCommutativeOperation.exponentiation.toBuilder().apply { leftOperand = nonCommutativeOperation.exponentiation.leftOperand.sort() @@ -291,22 +277,15 @@ private fun MathExpression.toComparableOperation(): ComparableOperation { BINARY_OPERATION -> when (binaryOperation.operator) { BinaryOperator.ADD -> toSummation(isRhsNegative = false) BinaryOperator.SUBTRACT -> toSummation(isRhsNegative = true) - BinaryOperator.MULTIPLY -> ComparableOperation.newBuilder().apply { - commutativeAccumulation = CommutativeAccumulation.newBuilder().apply { - accumulationType = AccumulationType.PRODUCT - addOperationToProduct(binaryOperation.leftOperand) - addOperationToProduct(binaryOperation.rightOperand) - }.build() - }.build() - BinaryOperator.DIVIDE -> - toNonCommutativeOperation(NonCommutativeOperation.Builder::setDivision) + BinaryOperator.MULTIPLY -> toProduct(isRhsInverted = false) + BinaryOperator.DIVIDE -> toProduct(isRhsInverted = true) BinaryOperator.EXPONENTIATE -> toNonCommutativeOperation(NonCommutativeOperation.Builder::setExponentiation) BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> ComparableOperation.getDefaultInstance() } UNARY_OPERATION -> when (unaryOperation.operator) { - UnaryOperator.NEGATE -> unaryOperation.operand.toComparableOperation().negate() + UnaryOperator.NEGATE -> unaryOperation.operand.toComparableOperation().makeNegative() UnaryOperator.POSITIVE -> unaryOperation.operand.toComparableOperation() UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> ComparableOperation.getDefaultInstance() @@ -335,13 +314,22 @@ private fun MathExpression.toSummation(isRhsNegative: Boolean): ComparableOperat }.build() } +private fun MathExpression.toProduct(isRhsInverted: Boolean): ComparableOperation { + return ComparableOperation.newBuilder().apply { + commutativeAccumulation = CommutativeAccumulation.newBuilder().apply { + accumulationType = AccumulationType.PRODUCT + addOperationToProduct(binaryOperation.leftOperand, forceInverse = false) + addOperationToProduct(binaryOperation.rightOperand, forceInverse = isRhsInverted) + }.build() + }.build() +} + private fun CommutativeAccumulation.Builder.addOperationToSum( - expression: MathExpression, - forceNegative: Boolean + expression: MathExpression, forceNegative: Boolean ) { - // If the whole operation is negative, carry it to the left-hand side of the operation. when (expression.binaryOperation.operator) { BinaryOperator.ADD -> { + // If the whole operation is negative, carry it to the left-hand side of the operation. addOperationToSum(expression.binaryOperation.leftOperand, forceNegative) addOperationToSum(expression.binaryOperation.rightOperand, forceNegative = false) } @@ -350,17 +338,28 @@ private fun CommutativeAccumulation.Builder.addOperationToSum( addOperationToSum(expression.binaryOperation.rightOperand, forceNegative = true) } else -> if (forceNegative) { - addCombinedOperations(expression.toComparableOperation().negate()) + addCombinedOperations(expression.toComparableOperation().makeNegative()) } else addCombinedOperations(expression.toComparableOperation()) } } -private fun CommutativeAccumulation.Builder.addOperationToProduct(expression: MathExpression) { - // This implies a binary operation. - if (expression.binaryOperation.operator == BinaryOperator.MULTIPLY) { - addOperationToProduct(expression.binaryOperation.leftOperand) - addOperationToProduct(expression.binaryOperation.rightOperand) - } else addCombinedOperations(expression.toComparableOperation()) +private fun CommutativeAccumulation.Builder.addOperationToProduct( + expression: MathExpression, forceInverse: Boolean +) { + when (expression.binaryOperation.operator) { + BinaryOperator.MULTIPLY -> { + // If the whole operation is inverted, carry it to the left-hand side of the operation. + addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) + addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = false) + } + BinaryOperator.DIVIDE -> { + addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) + addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = true) + } + else -> if (forceInverse) { + addCombinedOperations(expression.toComparableOperation().makeInverted()) + } else addCombinedOperations(expression.toComparableOperation()) + } } private fun MathExpression.toNonCommutativeOperation( @@ -380,12 +379,15 @@ private fun MathExpression.toNonCommutativeOperation( }.build() } -private fun ComparableOperation.positive(): ComparableOperation = +private fun ComparableOperation.makePositive(): ComparableOperation = toBuilder().apply { isNegated = false }.build() -private fun ComparableOperation.negate(): ComparableOperation = +private fun ComparableOperation.makeNegative(): ComparableOperation = toBuilder().apply { isNegated = true }.build() +private fun ComparableOperation.makeInverted(): ComparableOperation = + toBuilder().apply { isInverted = true }.build() + // TODO: move these to the UI layer & have them utilize non-translatable strings. private val numberFormat by lazy { NumberFormat.getNumberInstance(Locale.US) } private val singularOrdinalNames = mapOf( diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 1d83f1f5ccb..3f426b3f402 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -4703,6 +4703,7 @@ class NumericExpressionParserTest { val exp1 = parseNumericExpressionWithAllErrors("1") assertThat(exp1.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } @@ -4711,6 +4712,7 @@ class NumericExpressionParserTest { val exp2 = parseNumericExpressionWithAllErrors("-1") assertThat(exp2.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } @@ -4719,22 +4721,26 @@ class NumericExpressionParserTest { val exp3 = parseNumericExpressionWithAllErrors("1+3+4") assertThat(exp3.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(3) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(4) } @@ -4745,22 +4751,26 @@ class NumericExpressionParserTest { val exp4 = parseNumericExpressionWithAllErrors("-1-2-3") assertThat(exp4.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(3) index(0) { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } index(1) { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(2) } } index(2) { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } @@ -4771,22 +4781,26 @@ class NumericExpressionParserTest { val exp5 = parseNumericExpressionWithAllErrors("1+2-3") assertThat(exp5.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(3) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(2) } } index(2) { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } @@ -4797,22 +4811,26 @@ class NumericExpressionParserTest { val exp6 = parseNumericExpressionWithAllErrors("2*3*4") assertThat(exp6.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { hasOperandCountThat().isEqualTo(3) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(2) } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(4) } @@ -4823,20 +4841,24 @@ class NumericExpressionParserTest { val exp7 = parseNumericExpressionWithAllErrors("1-2*3") assertThat(exp7.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(2) index(0) { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { hasOperandCountThat().isEqualTo(2) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(2) } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } @@ -4845,6 +4867,7 @@ class NumericExpressionParserTest { } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } @@ -4855,20 +4878,24 @@ class NumericExpressionParserTest { val exp8 = parseNumericExpressionWithAllErrors("2*3-4") assertThat(exp8.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(2) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { hasOperandCountThat().isEqualTo(2) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(2) } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } @@ -4877,6 +4904,7 @@ class NumericExpressionParserTest { } index(1) { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(4) } @@ -4887,20 +4915,24 @@ class NumericExpressionParserTest { val exp9 = parseNumericExpressionWithAllErrors("1+2*3-4+8*7*6-9") assertThat(exp9.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(5) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { hasOperandCountThat().isEqualTo(2) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(2) } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } @@ -4909,22 +4941,26 @@ class NumericExpressionParserTest { } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { hasOperandCountThat().isEqualTo(3) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(6) } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(7) } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(8) } @@ -4933,18 +4969,21 @@ class NumericExpressionParserTest { } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } index(3) { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(4) } } index(4) { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(9) } @@ -4955,32 +4994,28 @@ class NumericExpressionParserTest { val exp10 = parseNumericExpressionWithAllErrors("2/3/4") assertThat(exp10.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() - nonCommutativeOperation { - division { - leftOperand { - hasNegatedPropertyThat().isFalse() - nonCommutativeOperation { - division { - leftOperand { - hasNegatedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) } - rightOperand { - hasNegatedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -4989,26 +5024,31 @@ class NumericExpressionParserTest { val exp11 = parseNumericExpressionWithoutOptionalErrors("2^3^4") assertThat(exp11.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() nonCommutativeOperation { exponentiation { leftOperand { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() nonCommutativeOperation { exponentiation { leftOperand { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(4) } @@ -5023,35 +5063,40 @@ class NumericExpressionParserTest { val exp12 = parseNumericExpressionWithAllErrors("1+2/3+3") assertThat(exp12.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(3) index(0) { hasNegatedPropertyThat().isFalse() - nonCommutativeOperation { - division { - leftOperand { - hasNegatedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) } - rightOperand { - hasNegatedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) } } } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } @@ -5062,35 +5107,40 @@ class NumericExpressionParserTest { val exp13 = parseNumericExpressionWithAllErrors("1+(2/3)+3") assertThat(exp13.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(3) index(0) { hasNegatedPropertyThat().isFalse() - nonCommutativeOperation { - division { - leftOperand { - hasNegatedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) } - rightOperand { - hasNegatedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) } } } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } @@ -5101,20 +5151,24 @@ class NumericExpressionParserTest { val exp14 = parseNumericExpressionWithAllErrors("1+2^3+3") assertThat(exp14.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(3) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() nonCommutativeOperation { exponentiation { leftOperand { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } @@ -5124,12 +5178,14 @@ class NumericExpressionParserTest { } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } @@ -5140,20 +5196,24 @@ class NumericExpressionParserTest { val exp15 = parseNumericExpressionWithAllErrors("1+(2^3)+3") assertThat(exp15.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(3) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() nonCommutativeOperation { exponentiation { leftOperand { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } @@ -5163,12 +5223,14 @@ class NumericExpressionParserTest { } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } @@ -5176,102 +5238,93 @@ class NumericExpressionParserTest { } } - // 2*3/4*7 is the same as ((2*3)/4)*7 due to precedence and associativity, so there's not much - // reordering possible. val exp16 = parseNumericExpressionWithAllErrors("2*3/4*7") assertThat(exp16.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) + hasOperandCountThat().isEqualTo(4) index(0) { hasNegatedPropertyThat().isFalse() - nonCommutativeOperation { - division { - leftOperand { - hasNegatedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(7) } } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } } } val exp17 = parseNumericExpressionWithAllErrors("2*(3/4)*7") assertThat(exp17.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) + hasOperandCountThat().isEqualTo(4) index(0) { hasNegatedPropertyThat().isFalse() - nonCommutativeOperation { - division { - leftOperand { - hasNegatedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(3) } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(7) } } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } } } val exp18 = parseNumericExpressionWithAllErrors("-3*sqrt(2)") assertThat(exp18.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { hasOperandCountThat().isEqualTo(2) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() nonCommutativeOperation { squareRootWithArgument { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(2) } @@ -5280,6 +5333,7 @@ class NumericExpressionParserTest { } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } @@ -5290,34 +5344,40 @@ class NumericExpressionParserTest { val exp19 = parseNumericExpressionWithAllErrors("1+(2+(3+(4+5)))") assertThat(exp19.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(5) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(2) } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } } index(3) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(4) } } index(4) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(5) } @@ -5328,34 +5388,40 @@ class NumericExpressionParserTest { val exp20 = parseNumericExpressionWithAllErrors("2*(3*(4*(5*6)))") assertThat(exp20.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { hasOperandCountThat().isEqualTo(5) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(2) } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(4) } } index(3) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(5) } } index(4) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(6) } @@ -5366,6 +5432,7 @@ class NumericExpressionParserTest { val exp21 = parseAlgebraicExpressionWithAllErrors("x") assertThat(exp21.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } @@ -5374,6 +5441,7 @@ class NumericExpressionParserTest { val exp22 = parseAlgebraicExpressionWithAllErrors("-x") assertThat(exp22.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } @@ -5382,22 +5450,26 @@ class NumericExpressionParserTest { val exp23 = parseAlgebraicExpressionWithAllErrors("1+x+y") assertThat(exp23.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(3) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("y") } @@ -5408,22 +5480,26 @@ class NumericExpressionParserTest { val exp24 = parseAlgebraicExpressionWithAllErrors("-1-x-y") assertThat(exp24.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(3) index(0) { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } index(1) { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } } index(2) { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("y") } @@ -5434,22 +5510,26 @@ class NumericExpressionParserTest { val exp25 = parseAlgebraicExpressionWithAllErrors("1+x-y") assertThat(exp25.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(3) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } } index(2) { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("y") } @@ -5460,22 +5540,26 @@ class NumericExpressionParserTest { val exp26 = parseAlgebraicExpressionWithAllErrors("2xy") assertThat(exp26.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { hasOperandCountThat().isEqualTo(3) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(2) } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("y") } @@ -5486,20 +5570,24 @@ class NumericExpressionParserTest { val exp27 = parseAlgebraicExpressionWithAllErrors("1-xy") assertThat(exp27.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(2) index(0) { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { hasOperandCountThat().isEqualTo(2) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("y") } @@ -5508,6 +5596,7 @@ class NumericExpressionParserTest { } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } @@ -5518,20 +5607,24 @@ class NumericExpressionParserTest { val exp28 = parseAlgebraicExpressionWithAllErrors("xy-4") assertThat(exp28.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(2) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { hasOperandCountThat().isEqualTo(2) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("y") } @@ -5540,6 +5633,7 @@ class NumericExpressionParserTest { } index(1) { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(4) } @@ -5550,20 +5644,24 @@ class NumericExpressionParserTest { val exp29 = parseAlgebraicExpressionWithAllErrors("1+xy-4+yz-9") assertThat(exp29.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(5) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { hasOperandCountThat().isEqualTo(2) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("y") } @@ -5572,16 +5670,19 @@ class NumericExpressionParserTest { } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { hasOperandCountThat().isEqualTo(2) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("y") } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("z") } @@ -5590,18 +5691,21 @@ class NumericExpressionParserTest { } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } index(3) { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(4) } } index(4) { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(9) } @@ -5612,32 +5716,28 @@ class NumericExpressionParserTest { val exp30 = parseAlgebraicExpressionWithAllErrors("2/x/y") assertThat(exp30.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() - nonCommutativeOperation { - division { - leftOperand { - hasNegatedPropertyThat().isFalse() - nonCommutativeOperation { - division { - leftOperand { - hasNegatedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - } - } + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) } - rightOperand { - hasNegatedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") } } } @@ -5646,26 +5746,31 @@ class NumericExpressionParserTest { val exp31 = parseAlgebraicExpressionWithoutOptionalErrors("x^3^4") assertThat(exp31.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() nonCommutativeOperation { exponentiation { leftOperand { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } } rightOperand { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() nonCommutativeOperation { exponentiation { leftOperand { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(4) } @@ -5680,35 +5785,40 @@ class NumericExpressionParserTest { val exp32 = parseAlgebraicExpressionWithAllErrors("1+x/y+z") assertThat(exp32.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(3) index(0) { hasNegatedPropertyThat().isFalse() - nonCommutativeOperation { - division { - leftOperand { - hasNegatedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") } - rightOperand { - hasNegatedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") } } } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("z") } @@ -5719,35 +5829,40 @@ class NumericExpressionParserTest { val exp33 = parseAlgebraicExpressionWithAllErrors("1+(x/y)+z") assertThat(exp33.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(3) index(0) { hasNegatedPropertyThat().isFalse() - nonCommutativeOperation { - division { - leftOperand { - hasNegatedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") } - rightOperand { - hasNegatedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") } } } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("z") } @@ -5758,20 +5873,24 @@ class NumericExpressionParserTest { val exp34 = parseAlgebraicExpressionWithAllErrors("1+x^3+y") assertThat(exp34.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(3) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() nonCommutativeOperation { exponentiation { leftOperand { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } } rightOperand { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } @@ -5781,12 +5900,14 @@ class NumericExpressionParserTest { } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("y") } @@ -5797,20 +5918,24 @@ class NumericExpressionParserTest { val exp35 = parseAlgebraicExpressionWithAllErrors("1+(x^3)+y") assertThat(exp35.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(3) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() nonCommutativeOperation { exponentiation { leftOperand { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } } rightOperand { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } @@ -5820,12 +5945,14 @@ class NumericExpressionParserTest { } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("y") } @@ -5833,102 +5960,93 @@ class NumericExpressionParserTest { } } - // 2*3/4*7 is the same as ((2*3)/4)*7 due to precedence and associativity, so there's not much - // reordering possible. val exp36 = parseAlgebraicExpressionWithAllErrors("2*x/y*z") assertThat(exp36.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) + hasOperandCountThat().isEqualTo(4) index(0) { hasNegatedPropertyThat().isFalse() - nonCommutativeOperation { - division { - leftOperand { - hasNegatedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("z") } } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } } } val exp37 = parseAlgebraicExpressionWithAllErrors("2*(x/y)*z") assertThat(exp37.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) + hasOperandCountThat().isEqualTo(4) index(0) { hasNegatedPropertyThat().isFalse() - nonCommutativeOperation { - division { - leftOperand { - hasNegatedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) } } index(1) { hasNegatedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("z") } } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } } } val exp38 = parseAlgebraicExpressionWithAllErrors("-2*sqrt(x)") assertThat(exp38.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { hasOperandCountThat().isEqualTo(2) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() nonCommutativeOperation { squareRootWithArgument { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } @@ -5937,6 +6055,7 @@ class NumericExpressionParserTest { } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(2) } @@ -5947,34 +6066,40 @@ class NumericExpressionParserTest { val exp39 = parseAlgebraicExpressionWithAllErrors("1+(x+(3+(z+y)))") assertThat(exp39.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { hasOperandCountThat().isEqualTo(5) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(3) } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } } index(3) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("y") } } index(4) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("z") } @@ -5985,34 +6110,40 @@ class NumericExpressionParserTest { val exp40 = parseAlgebraicExpressionWithAllErrors("2*(x*(4*(zy)))") assertThat(exp40.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { hasOperandCountThat().isEqualTo(5) index(0) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(2) } } index(1) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(4) } } index(2) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } } index(3) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("y") } } index(4) { hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("z") } @@ -6055,7 +6186,11 @@ class NumericExpressionParserTest { val list17 = createComparableOperationListFromNumericExpression("2*3/4") val list18 = createComparableOperationListFromNumericExpression("3/4*2") - assertThat(list17).isNotEqualTo(list18) + assertThat(list17).isEqualTo(list18) + + val list45 = createComparableOperationListFromNumericExpression("2*3/4") + val list46 = createComparableOperationListFromNumericExpression("2*3*4") + assertThat(list45).isNotEqualTo(list46) val list19 = createComparableOperationListFromNumericExpression("2*3/4") val list20 = createComparableOperationListFromNumericExpression("2*4/3") @@ -6063,7 +6198,7 @@ class NumericExpressionParserTest { val list21 = createComparableOperationListFromNumericExpression("2*3/4*7") val list22 = createComparableOperationListFromNumericExpression("3/4*7*2") - assertThat(list21).isNotEqualTo(list22) + assertThat(list21).isEqualTo(list22) val list23 = createComparableOperationListFromNumericExpression("2*3/4*7") val list24 = createComparableOperationListFromNumericExpression("7*(3*2/4)") @@ -6071,7 +6206,7 @@ class NumericExpressionParserTest { val list25 = createComparableOperationListFromNumericExpression("2*3/4*7") val list26 = createComparableOperationListFromNumericExpression("7*3*2/4") - assertThat(list25).isNotEqualTo(list26) + assertThat(list25).isEqualTo(list26) val list27 = createComparableOperationListFromNumericExpression("-2*3") val list28 = createComparableOperationListFromNumericExpression("3*-2") @@ -6432,6 +6567,8 @@ class NumericExpressionParserTest { ) { fun hasNegatedPropertyThat(): BooleanSubject = assertThat(operation.isNegated) + fun hasInvertedPropertyThat(): BooleanSubject = assertThat(operation.isInverted) + fun commutativeAccumulationWithType( type: ComparableOperationList.CommutativeAccumulation.AccumulationType, init: CommutativeAccumulationComparator.() -> Unit @@ -6487,11 +6624,6 @@ class NumericExpressionParserTest { class NonCommutativeOperationComparator private constructor( private val operation: ComparableOperationList.NonCommutativeOperation ) { - fun division(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - verifyTypeAs(ComparableOperationList.NonCommutativeOperation.OperationTypeCase.DIVISION) - return BinaryOperationComparator.createFrom(operation.division).also(init) - } - fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { verifyTypeAs( ComparableOperationList.NonCommutativeOperation.OperationTypeCase.EXPONENTIATION From 42e7683feddf109001b68e3919e7e6cd9f824e5a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 13 Dec 2021 18:25:12 -0800 Subject: [PATCH 088/289] Add complete support for polynomials. Or, at least, what we're willing to implement in the MVP. This doesn't include factoring, and generally exponentiation is a bit limited, but all other operations are fully functional (including polynomial division). --- model/src/main/proto/math.proto | 2 +- .../util/math/MathExpressionExtensions.kt | 439 ++++++-- .../util/math/NumericExpressionParserTest.kt | 937 ++++++++++++++++++ 3 files changed, 1271 insertions(+), 107 deletions(-) diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 803fdff16b8..c52036b9e93 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -152,7 +152,7 @@ message Polynomial { message Variable { string name = 1; - int32 power = 2; + uint32 power = 2; } } } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 808ba59c5f3..6f73b8a99af 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -71,7 +71,7 @@ private val COMPARABLE_OPERATION_COMPARATOR: Comparator by Comparator.comparing(ComparableOperation::getComparisonTypeCase) .thenComparing(ComparableOperation::getIsNegated) .thenComparing(ComparableOperation::getIsInverted) - .selectAmong( + .thenSelectAmong( ComparableOperation::getComparisonTypeCase, COMMUTATIVE_ACCUMULATION to comparingDeferred( ComparableOperation::getCommutativeAccumulation @@ -88,7 +88,7 @@ private val COMMUTATIVE_ACCUMULATION_COMPARATOR: Comparator accumulation.combinedOperationsList.toSortedSet(COMPARABLE_OPERATION_COMPARATOR) - }, ::compareSets) + }, COMPARABLE_OPERATION_COMPARATOR.toSetComparator()) } private val NON_COMMUTATIVE_BINARY_OPERATION_COMPARATOR by lazy { @@ -101,7 +101,7 @@ private val NON_COMMUTATIVE_BINARY_OPERATION_COMPARATOR by lazy { private val NON_COMMUTATIVE_OPERATION_COMPARATOR: Comparator by lazy { Comparator.comparing(NonCommutativeOperation::getOperationTypeCase) - .selectAmong( + .thenSelectAmong( NonCommutativeOperation::getOperationTypeCase, OperationTypeCase.EXPONENTIATION to Comparator.comparing( NonCommutativeOperation::getExponentiation, NON_COMMUTATIVE_BINARY_OPERATION_COMPARATOR @@ -114,6 +114,29 @@ private val NON_COMMUTATIVE_OPERATION_COMPARATOR: Comparator by lazy { Comparator.comparing(Real::toDouble) } +private val POLYNOMIAL_VARIABLE_COMPARATOR: Comparator by lazy { + // Note that power is reversed because larger powers should actually be sorted ahead of smaller + // powers for the same variable name (but variable name still takes precedence). This ensures + // cases like x^2y+y^2x are sorted in that order. + Comparator.comparing(Variable::getName).thenComparingReversed(Variable::getPower) +} + +private val POLYNOMIAL_TERM_COMPARATOR: Comparator by lazy { + // First, sort by all variable names to ensure xy is placed ahead of xz. Then, sort by variable + // powers in order of the variables (such that x^2y is ranked higher thank xy). Finally, sort by + // the coefficient to ensure equality through the comparator works correctly (though in practice + // like terms should always be combined). Note the specific reversing happening here. It's done in + // this way so that sorted set bigger/smaller list is reversed (which matches expectations since + // larger terms should appear earlier in the results). This is implementing an ordering similar to + // https://en.wikipedia.org/wiki/Polynomial#Definition, except for multi-variable functions (where + // variables of higher degree are preferred over lower degree by lexicographical order of variable + // names). + Comparator.comparing>( + { term -> term.variableList.toSortedSet(POLYNOMIAL_VARIABLE_COMPARATOR) }, + POLYNOMIAL_VARIABLE_COMPARATOR.reversed().toSetComparator() + ).reversed().thenComparing(Term::getCoefficient, REAL_COMPARATOR) +} + private fun comparingDeferred( keySelector: (T) -> U, comparatorSelector: () -> Comparator ): Comparator { @@ -124,7 +147,11 @@ private fun comparingDeferred( } } -private fun > Comparator.selectAmong( +private fun > Comparator.thenComparingReversed( + keySelector: (T) -> U +): Comparator = thenComparing(Comparator.comparing(keySelector).reversed()) + +private fun > Comparator.thenSelectAmong( enumSelector: (T) -> E, vararg comparators: Pair> ): Comparator { val comparatorMap = comparators.toMap() @@ -135,28 +162,30 @@ private fun > Comparator.selectAmong( check(enum1 == enum2) { "Expected objects to have the same enum values: $o1 ($enum1), $o2 ($enum2)" } - val comparator = checkNotNull(comparatorMap[enum1]) { "No comparator for matched enum: $enum1" } + val comparator = + checkNotNull(comparatorMap[enum1]) { "No comparator for matched enum: $enum1" } return@Comparator comparator.compare(o1, o2) } ) } -private fun compareSets( - first: SortedSet, second: SortedSet -): Int { - // Reference: https://stackoverflow.com/a/30107086. - val firstIter = first.iterator() - val secondIter = second.iterator() - while (firstIter.hasNext() || secondIter.hasNext()) { - val comparison = COMPARABLE_OPERATION_COMPARATOR.compare(firstIter.next(), secondIter.next()) - if (comparison != 0) return comparison // Found a different item. - } +private fun Comparator.toSetComparator(): Comparator> { + val itemComparator = this + return Comparator { first, second -> + // Reference: https://stackoverflow.com/a/30107086. + val firstIter = first.iterator() + val secondIter = second.iterator() + while (firstIter.hasNext() && secondIter.hasNext()) { + val comparison = itemComparator.compare(firstIter.next(), secondIter.next()) + if (comparison != 0) return@Comparator comparison // Found a different item. + } - // Everything is equal up to here, see if the lists are different length. - return when { - firstIter.hasNext() -> 1 // The first list is longer, therefore "greater." - secondIter.hasNext() -> -1 // Ditto, but for the second list. - else -> 0 // Otherwise, they're the same length with all equal items (and are thus equal). + // Everything is equal up to here, see if the lists are different length. + return@Comparator when { + firstIter.hasNext() -> 1 // The first list is longer, therefore "greater." + secondIter.hasNext() -> -1 // Ditto, but for the second list. + else -> 0 // Otherwise, they're the same length with all equal items (and are thus equal). + } } } @@ -266,6 +295,47 @@ private fun MathExpression.stripGroups(): MathExpression { } } +private val ONE_HALF by lazy { + Real.newBuilder().apply { + rational = Fraction.newBuilder().apply { + numerator = 1 + denominator = 2 + }.build() + }.build() +} + +private fun MathExpression.replaceSquareRoots(): MathExpression { + return when (expressionTypeCase) { + BINARY_OPERATION -> toBuilder().apply { + binaryOperation = binaryOperation.toBuilder().apply { + leftOperand = binaryOperation.leftOperand.replaceSquareRoots() + rightOperand = binaryOperation.rightOperand.replaceSquareRoots() + }.build() + }.build() + UNARY_OPERATION -> toBuilder().apply { + unaryOperation = unaryOperation.toBuilder().apply { + operand = unaryOperation.operand.replaceSquareRoots() + }.build() + }.build() + FUNCTION_CALL -> when (functionCall.functionType) { + FunctionType.SQUARE_ROOT -> toBuilder().apply { + // Replace the square root function call with the equivalent exponentiation. That is, + // sqrt(x)=x^(1/2). + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = BinaryOperator.EXPONENTIATE + leftOperand = functionCall.argument.replaceSquareRoots() + rightOperand = MathExpression.newBuilder().apply { + constant = ONE_HALF + }.build() + }.build() + }.build() + FunctionType.FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> this + } + GROUP -> group.replaceSquareRoots() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> this + } +} + private fun MathExpression.toComparableOperation(): ComparableOperation { return when (expressionTypeCase) { CONSTANT -> ComparableOperation.newBuilder().apply { @@ -680,9 +750,16 @@ fun MathExpression.toPolynomial(): Polynomial? { // e. Multiplication (for one side constant, apply to coefficients otherwise perform polynomial multiplication) // f. Addition (treat constants as constant terms & concatenate term lists to compute new polynomial) // 4. Collect the final polynomial as the result. Early exiting indicates a non-polynomial. - return reduceToPolynomial() + return stripGroups().replaceSquareRoots() + .reduceToPolynomial() + ?.removeUnnecessaryVariables() + ?.sort() } +private fun Polynomial.sort() = Polynomial.newBuilder().apply { + addAllTerm(this@sort.termList.sortedWith(POLYNOMIAL_TERM_COMPARATOR)) +}.build() + fun Polynomial.isUnivariate(): Boolean = getUniqueVariableCount() == 1 fun Polynomial.isMultivariate(): Boolean = getUniqueVariableCount() > 1 @@ -691,31 +768,40 @@ private fun Polynomial.getUniqueVariableCount(): Int { return termList.flatMap(Term::getVariableList).map(Variable::getName).toSet().size } -fun Polynomial.toAnswerString(): String { - return termList.joinToString(separator = " + ", transform = Term::toAnswerString) +fun Polynomial.toPlainText(): String { + return termList.map { it.toPlainText() }.reduce { acc, termAnswerStr -> + if (termAnswerStr.startsWith("-")) { + "$acc - ${termAnswerStr.drop(1)}" + } else "$acc + $termAnswerStr" + } } -private fun Term.toAnswerString(): String { +private fun Term.toPlainText(): String { val productValues = mutableListOf() // Include the coefficient if there is one (coefficients of 1 are ignored only if there are // variables present). - if (!coefficient.isApproximatelyEqualTo(1.0) || variableList.isEmpty()) { - productValues += coefficient.toAnswerString() + productValues += when { + variableList.isEmpty() || !abs(coefficient).isApproximatelyEqualTo(1.0) -> when { + coefficient.isRational() && variableList.isNotEmpty() -> "(${coefficient.toPlainText()})" + else -> coefficient.toPlainText() + } + coefficient.isNegative() -> "-" + else -> "" } // Include any present variables. - productValues += variableList.map(Variable::toAnswerString) + productValues += variableList.map(Variable::toPlainText) // Take the product of all relevant values of the term. - return productValues.joinToString(separator = "*") + return productValues.joinToString(separator = "") } -private fun Variable.toAnswerString(): String { +private fun Variable.toPlainText(): String { return if (power > 1) "$name^$power" else name } -private fun Real.toAnswerString(): String = when (realTypeCase) { +private fun Real.toPlainText(): String = when (realTypeCase) { // Note that the rational part is first converted to an improper fraction since mixed fractions // can't be expressed as a single coefficient in typical polynomial syntax). RATIONAL -> rational.toImproperForm().toAnswerString() @@ -733,18 +819,20 @@ private fun Real.toPlainString(): String = when (realTypeCase) { private fun MathExpression.reduceToPolynomial(): Polynomial? { return when (expressionTypeCase) { - CONSTANT -> createPolynomialFromConstant(constant) - VARIABLE -> createSingleTermPolynomial(variable) + CONSTANT -> createConstantPolynomial(constant) + VARIABLE -> createSingleVariablePolynomial(variable) UNARY_OPERATION -> unaryOperation.reduceToPolynomial() BINARY_OPERATION -> binaryOperation.reduceToPolynomial() - else -> null + // Both functions & groups should be removed ahead of polynomial reduction. + FUNCTION_CALL, GROUP, EXPRESSIONTYPE_NOT_SET, null -> null } } private fun MathUnaryOperation.reduceToPolynomial(): Polynomial? { return when (operator) { UnaryOperator.NEGATE -> -(operand.reduceToPolynomial() ?: return null) - else -> null + UnaryOperator.POSITIVE -> operand.reduceToPolynomial() // Positive unary changes nothing. + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> null } } @@ -757,14 +845,12 @@ private fun MathBinaryOperation.reduceToPolynomial(): Polynomial? { BinaryOperator.MULTIPLY -> leftPolynomial * rightPolynomial BinaryOperator.DIVIDE -> leftPolynomial / rightPolynomial BinaryOperator.EXPONENTIATE -> leftPolynomial.pow(rightPolynomial) - else -> null + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null } } /** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ -private fun Polynomial.isConstant(): Boolean { - return termCount == 1 && getTerm(0).variableCount == 0 -} +fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 /** * Returns the first term coefficient from this polynomial. This corresponds to the whole value of @@ -773,9 +859,7 @@ private fun Polynomial.isConstant(): Boolean { * Note that this function can throw if the polynomial is empty (so isConstant() should always be * checked first). */ -private fun Polynomial.getConstant(): Real { - return getTerm(0).coefficient -} +fun Polynomial.getConstant(): Real = getTerm(0).coefficient private operator fun Polynomial.unaryMinus(): Polynomial { // Negating a polynomial just requires flipping the signs on all coefficients. @@ -785,9 +869,13 @@ private operator fun Polynomial.unaryMinus(): Polynomial { .build() } +// TODO: extract the filtering done during addition & also do it at the end in case initial polynomials are tried (like x^0, or 0y). Add tests for these cases. private operator fun Polynomial.plus(rhs: Polynomial): Polynomial { - // Adding two polynomials just requires combining their terms lists. - return Polynomial.newBuilder().addAllTerm(termList).addAllTerm(rhs.termList).build() + // Adding two polynomials just requires combining their terms lists (taking into account combining + // common terms). + return Polynomial.newBuilder().apply { + addAllTerm(this@plus.termList + rhs.termList) + }.build().combineLikeTerms().removeUnnecessaryVariables() } private operator fun Polynomial.minus(rhs: Polynomial): Polynomial { @@ -797,14 +885,13 @@ private operator fun Polynomial.minus(rhs: Polynomial): Polynomial { private operator fun Polynomial.times(rhs: Polynomial): Polynomial { // Polynomial multiplication is simply multiplying each term in one by each term in the other. - // TODO: ensure this properly computes trivial cases like (x^2 becoming x-squared) or whether - // those cases need to be special cased. - return Polynomial.newBuilder() - .addAllTerm( - termList.flatMap { leftTerm -> - rhs.termList.map { rightTerm -> leftTerm * rightTerm } - } - ).build() + val crossMultipliedTerms = termList.flatMap { leftTerm -> + rhs.termList.map { rightTerm -> leftTerm * rightTerm } + } + + // Treat each multiplied term as a unique polynomial, then add them together (so that like terms + // can be properly combined). + return crossMultipliedTerms.map { createSingleTermPolynomial(it) }.reduce(Polynomial::plus) } private operator fun Term.times(rhs: Term): Term { @@ -837,26 +924,34 @@ private operator fun Polynomial.div(rhs: Polynomial): Polynomial? { // (1/2)x + (3/2). // See https://en.wikipedia.org/wiki/Polynomial_long_division#Pseudocode for reference. if (rhs.isApproximatelyZero()) { - // TODO: test (x+2)/0 return null // Dividing by zero is invalid and thus cannot yield a polynomial. } - var quotient = createPolynomialFromConstant(createCoefficientValueOf(value = 0)) + var quotient = createConstantPolynomial(createCoefficientValueOfZero()) var remainder = this - val divisorDegree = rhs.getDegree() val leadingDivisorTerm = rhs.getLeadingTerm() + val divisorVariable = leadingDivisorTerm.highestDegreeVariable() + val divisorVariableName = divisorVariable?.name + val divisorDegree = leadingDivisorTerm.highestDegree() while (!remainder.isApproximatelyZero() && remainder.getDegree() >= divisorDegree) { - // Attempt to divide the leading terms (this may fail). - val newTerm = remainder.getLeadingTerm() / leadingDivisorTerm ?: return null + // Attempt to divide the leading terms (this may fail). Note that the leading term should always + // be based on the divisor variable being used (otherwise subsequent division steps will be + // inconsistent and potentially fail to resolve). + val newTerm = + remainder.getLeadingTerm(matchedVariable = divisorVariableName) / leadingDivisorTerm + ?: return null quotient += newTerm.toPolynomial() remainder -= newTerm.toPolynomial() * rhs } - if (!remainder.isApproximatelyZero()) { - // A non-zero remainder indicates the division was not "pure" which means the result is a - // non-polynomial. - return null + return when { + remainder.isApproximatelyZero() -> quotient // Exact division (i.e. with no remainder). + remainder.isConstant() && rhs.isConstant() -> { + // Remainder is a constant term. + val remainingTerm = remainder.getConstant() / rhs.getConstant() + quotient + createConstantPolynomial(remainingTerm) + } + else -> null // Remainder is a polynomial, so the division failed. } - return quotient } private fun Term.toPolynomial(): Polynomial { @@ -887,6 +982,48 @@ private operator fun Term.div(rhs: Term): Term? { .build() } +private fun Polynomial.combineLikeTerms(): Polynomial { + // The following algorithm is expected to grow in space by O(N*M) and in time by O(N*m*log(m)) + // where N is the total number of terms, M is the total number of variables, and m is the largest + // single count of variables among all terms (this is assuming constant-time insertion for the + // underlying hashtable). + val newTerms = termList.groupBy { + it.variableList.sortedWith(POLYNOMIAL_VARIABLE_COMPARATOR) + }.mapValues { (_, coefficientTerms) -> + coefficientTerms.map { it.coefficient } + }.mapNotNull { (variables, coefficients) -> + // Combine like terms by summing their coefficients. + val newCoefficient = coefficients.reduce(Real::plus) + return@mapNotNull if (!newCoefficient.isApproximatelyZero()) { + Term.newBuilder().apply { + coefficient = newCoefficient + + // Remove variables with zero powers (since they evaluate to '1'). + addAllVariable(variables.filter { variable -> variable.power != 0 }) + }.build() + } else null // Zero terms should be removed. + } + return Polynomial.newBuilder().apply { + addAllTerm(newTerms) + }.build().ensureAtLeastConstant() +} + +private fun Polynomial.removeUnnecessaryVariables(): Polynomial { + return Polynomial.newBuilder().apply { + addAllTerm(this@removeUnnecessaryVariables.termList.filter { term -> + !term.coefficient.isApproximatelyZero() + }) + }.build().ensureAtLeastConstant() +} + +private fun Polynomial.ensureAtLeastConstant(): Polynomial { + return if (termCount == 0) { + Polynomial.newBuilder().apply { + addTerm(createZeroTerm()) + }.build() + } else this +} + private fun List.toPowerMap(): Map { return associateBy({ it.name }, { it.power }) } @@ -895,9 +1032,13 @@ private fun Map.toVariableList(): List { return map { (name, power) -> Variable.newBuilder().setName(name).setPower(power).build() } } -private fun Polynomial.getLeadingTerm(): Term { +private fun Polynomial.getLeadingTerm(matchedVariable: String? = null): Term { // Return the leading term. Reference: https://undergroundmathematics.org/glossary/leading-term. - return termList.reduce { maxTerm, term -> + return termList.filter { term -> + matchedVariable?.let { variableName -> + term.variableList.any { it.name == variableName } + } ?: true + }.reduce { maxTerm, term -> val maxTermDegree = maxTerm.highestDegree() val termDegree = term.highestDegree() return@reduce if (termDegree > maxTermDegree) term else maxTerm @@ -908,77 +1049,135 @@ private fun Polynomial.getLeadingTerm(): Term { // https://www.varsitytutors.com/algebra_1-help/how-to-find-the-degree-of-a-polynomial. private fun Polynomial.getDegree(): Int = getLeadingTerm().highestDegree() -private fun Term.highestDegree(): Int { - return variableList.map(Variable::getPower).maxOrNull() ?: 0 -} +private fun Term.highestDegreeVariable(): Variable? = variableList.maxByOrNull(Variable::getPower) + +private fun Term.highestDegree(): Int = highestDegreeVariable()?.power ?: 0 + +private fun Term.pow(rational: Fraction): Term? { + // Raising an exponent by an exponent just requires multiplying the two together. + val newVariablePowers = variableList.map { variable -> + variable.power.toWholeNumberFraction() * rational + } + + // If any powers are not whole numbers then the rational is likely representing a root and the + // term in question is not rootable to that degree. + if (newVariablePowers.any { !it.isOnlyWholeNumber() }) return null -private fun Polynomial.isApproximatelyZero(): Boolean { - return isConstant() && getConstant().isApproximatelyZero() + return Term.newBuilder().apply { + coefficient = this@pow.coefficient + addAllVariable( + this@pow.variableList.zip(newVariablePowers).map { (variable, newPower) -> + variable.toBuilder().apply { + power = newPower.toWholeNumber() + }.build() + } + ) + }.build() } +private fun Polynomial.isApproximatelyZero(): Boolean = + termList.all { it.coefficient.isApproximatelyZero() } // Zero polynomials only have 0 coefs. + private fun Polynomial.pow(exp: Int): Polynomial { // Anything raised to the power of 0 is 1. - if (exp == 0) return createPolynomialFromConstant(createCoefficientValueOfOne()) + if (exp == 0) return createConstantPolynomial(createCoefficientValueOfOne()) if (exp == 1) return this var newValue = this for (i in 1 until exp) newValue *= this return newValue } -private fun Polynomial.pow(exp: Real): Polynomial? { - // Polynomials can only be raised to positive integers (or zero). - return if (exp.hasRational() && exp.rational.isOnlyWholeNumber() && !exp.rational.isNegative) { - pow(exp.rational.wholeNumber) +private fun Polynomial.pow(rational: Fraction): Polynomial? { + // Polynomials with addition require factoring. + return if (isSingleTerm()) { + termList.first().pow(rational)?.toPolynomial() } else null } +private fun Polynomial.pow(exp: Real): Polynomial? { + val shouldBeInverted = exp.isNegative() + val positivePower = if (shouldBeInverted) -exp else exp + val exponentiation = when { + // Constant polynomials can be raised by any constant. + isConstant() -> createConstantPolynomial(getConstant().pow(positivePower)) + + // Polynomials can only be raised to positive integers (or zero). + exp.isWholeNumber() -> exp.asWholeNumber()?.let { pow(it) } + + // Polynomials can potentially be raised by a fractional power. + exp.isRational() -> pow(exp.rational) + + // All other cases require factoring will definitely not compute to polynomials (such as + // irrational exponents). + else -> null + } + return if (shouldBeInverted) { + val onePolynomial = createConstantPolynomial(createCoefficientValueOfOne()) + // Note that this division is guaranteed to fail if the exponentiation result is a polynomial. + // Future implementations may leverage root-finding algorithms to factor for integer inverse + // powers (such as square root, cubic root, etc.). Non-integer inverse powers will require + // sampling. + exponentiation?.let { onePolynomial / it } + } else exponentiation +} + private fun Polynomial.pow(exp: Polynomial): Polynomial? { // Polynomial exponentiation is only supported if the right side is a constant polynomial, - // otherwise the result cannot be a polynomial. + // otherwise the result cannot be a polynomial (though could still be compared to another + // expression by utilizing sampling techniques). return if (exp.isConstant()) pow(exp.getConstant()) else null } -private fun MathExpression.toTreeNode(): ExpressionTreeNode { - return when (expressionTypeCase) { - CONSTANT -> ExpressionTreeNode.ConstantNode(constant) - VARIABLE -> ExpressionTreeNode.PolynomialNode(createSingleTermPolynomial(variable)) - UNARY_OPERATION -> ExpressionTreeNode.ExpressionNode(this, unaryOperation.collectChildren()) - BINARY_OPERATION -> ExpressionTreeNode.ExpressionNode(this, binaryOperation.collectChildren()) - else -> ExpressionTreeNode.ExpressionNode(this, mutableListOf()) - } -} +private fun Polynomial.isSingleTerm(): Boolean = termList.size == 1 -private fun MathUnaryOperation.collectChildren(): MutableList { - return mutableListOf(operand.toTreeNode()) +//private fun MathExpression.toTreeNode(): ExpressionTreeNode { +// return when (expressionTypeCase) { +// CONSTANT -> ExpressionTreeNode.ConstantNode(constant) +// VARIABLE -> ExpressionTreeNode.PolynomialNode(createSingleTermPolynomial(variable)) +// UNARY_OPERATION -> ExpressionTreeNode.ExpressionNode(this, unaryOperation.collectChildren()) +// BINARY_OPERATION -> ExpressionTreeNode.ExpressionNode(this, binaryOperation.collectChildren()) +// else -> ExpressionTreeNode.ExpressionNode(this, mutableListOf()) +// } +//} +// +//private fun MathUnaryOperation.collectChildren(): MutableList { +// return mutableListOf(operand.toTreeNode()) +//} +// +//private fun MathBinaryOperation.collectChildren(): MutableList { +// return mutableListOf(leftOperand.toTreeNode(), rightOperand.toTreeNode()) +//} + +private fun createSingleVariablePolynomial(variableName: String): Polynomial { + return createSingleTermPolynomial( + Term.newBuilder().apply { + coefficient = createCoefficientValueOfOne() + addVariable(Variable.newBuilder().apply { + name = variableName + power = 1 + }.build()) + }.build() + ) } -private fun MathBinaryOperation.collectChildren(): MutableList { - return mutableListOf(leftOperand.toTreeNode(), rightOperand.toTreeNode()) -} +private fun createConstantPolynomial(constant: Real) = + createSingleTermPolynomial(Term.newBuilder().setCoefficient(constant).build()) -private fun createSingleTermPolynomial(variableName: String): Polynomial { - return Polynomial.newBuilder() - .addTerm( - Term.newBuilder() - .setCoefficient(createCoefficientValueOfOne()) - .addVariable(Variable.newBuilder().setName(variableName).setPower(1)) - ).build() -} +private fun createSingleTermPolynomial(term: Term) = + Polynomial.newBuilder().apply { addTerm(term) }.build() -private fun createPolynomialFromConstant(constant: Real): Polynomial { - return Polynomial.newBuilder() - .addTerm(Term.newBuilder().setCoefficient(constant)) - .build() -} +private fun createCoefficientValueOf(value: Int) = Real.newBuilder().apply { + integer = value +}.build() -private fun createCoefficientValueOf(value: Int): Real { - return Real.newBuilder() - .setRational(Fraction.newBuilder().setWholeNumber(value).setDenominator(1)) - .build() -} +private fun createCoefficientValueOfZero(): Real = createCoefficientValueOf(value = 0) private fun createCoefficientValueOfOne(): Real = createCoefficientValueOf(value = 1) +private fun createZeroTerm() = Term.newBuilder().apply { + coefficient = createCoefficientValueOfZero() +}.build() + private sealed class ExpressionTreeNode { data class ExpressionNode( val mathExpression: MathExpression, @@ -1211,8 +1410,36 @@ private fun sqrt(real: Real): Real { } } +private fun abs(real: Real): Real = if (real.isNegative()) -real else real + private fun Real.isInteger(): Boolean = realTypeCase == INTEGER +private fun Real.isNegative(): Boolean = when (realTypeCase) { + RATIONAL -> rational.isNegative + IRRATIONAL -> irrational < 0 + INTEGER -> integer < 0 + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") +} + +private fun Real.asWholeNumber(): Int? { + return when (realTypeCase) { + RATIONAL -> if (rational.isOnlyWholeNumber()) rational.toWholeNumber() else null + INTEGER -> integer + IRRATIONAL -> null + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + } +} + +private fun Real.isWholeNumber(): Boolean { + return when (realTypeCase) { + RATIONAL -> rational.isOnlyWholeNumber() + INTEGER -> true + IRRATIONAL, REALTYPE_NOT_SET, null -> false + } +} + +private fun Real.isRational(): Boolean = realTypeCase == RATIONAL + private fun Double.pow(rhs: Fraction): Double = this.pow(rhs.toDouble()) private fun Fraction.pow(rhs: Double): Double = toDouble().pow(rhs) private operator fun Double.plus(rhs: Fraction): Double = this + rhs.toFloat() diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 3f426b3f402..bbc67d1d775 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -41,6 +41,7 @@ import org.oppia.android.app.model.OppiaLanguage.HINGLISH import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED +import org.oppia.android.app.model.Polynomial import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError @@ -6247,12 +6248,887 @@ class NumericExpressionParserTest { // TODO: add tests for comparator/sorting & negation simplification? } + @Test + fun testPolynomials() { + // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). + + val poly1 = parseNumericExpressionWithAllErrors("1").toPolynomial() + assertThat(poly1).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly1).isConstantThat().isIntegerThat().isEqualTo(1) + + val poly13 = parseNumericExpressionWithAllErrors("1-1").toPolynomial() + assertThat(poly13).evaluatesToPlainTextThat().isEqualTo("0") + assertThat(poly13).isConstantThat().isIntegerThat().isEqualTo(0) + + val poly2 = parseNumericExpressionWithAllErrors("3 + 4 * 2 / (1 - 5) ^ 2").toPolynomial() + assertThat(poly2).evaluatesToPlainTextThat().isEqualTo("7/2") + assertThat(poly2).isConstantThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(3) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + + val poly3 = parseAlgebraicExpressionWithAllErrors("133+3.14*x/(11-15)^2").toPolynomial() + assertThat(poly3).evaluatesToPlainTextThat().isEqualTo("0.19625x + 133") + assertThat(poly3).hasTermCountThat().isEqualTo(2) + assertThat(poly3).term(0).hasCoefficientThat().isIrrationalThat().isWithin(1e-5).of(0.19625) + assertThat(poly3).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly3).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly3).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly3).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(133) + assertThat(poly3).term(1).hasVariableCountThat().isEqualTo(0) + + val poly4 = parseAlgebraicExpressionWithAllErrors("x^2").toPolynomial() + assertThat(poly4).evaluatesToPlainTextThat().isEqualTo("x^2") + assertThat(poly4).hasTermCountThat().isEqualTo(1) + assertThat(poly4).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly4).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly4).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly4).term(0).variable(0).hasPowerThat().isEqualTo(2) + + val poly5 = parseAlgebraicExpressionWithAllErrors("xy+x").toPolynomial() + assertThat(poly5).evaluatesToPlainTextThat().isEqualTo("xy + x") + assertThat(poly5).hasTermCountThat().isEqualTo(2) + assertThat(poly5).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly5).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly5).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly5).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly5).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly5).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly5).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly5).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly5).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly5).term(1).variable(0).hasPowerThat().isEqualTo(1) + + val poly6 = parseAlgebraicExpressionWithAllErrors("2x").toPolynomial() + assertThat(poly6).evaluatesToPlainTextThat().isEqualTo("2x") + assertThat(poly6).hasTermCountThat().isEqualTo(1) + assertThat(poly6).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly6).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) + assertThat(poly6).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly6).term(0).variable(0).hasPowerThat().isEqualTo(1) + + val poly30 = parseAlgebraicExpressionWithAllErrors("x+2").toPolynomial() + assertThat(poly30).evaluatesToPlainTextThat().isEqualTo("x + 2") + assertThat(poly30).hasTermCountThat().isEqualTo(2) + assertThat(poly30).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly30).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(0) + } + + val poly29 = parseAlgebraicExpressionWithAllErrors("x^2-3*x-10").toPolynomial() + assertThat(poly29).evaluatesToPlainTextThat().isEqualTo("x^2 - 3x - 10") + assertThat(poly29).hasTermCountThat().isEqualTo(3) + assertThat(poly29).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly29).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly29).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-10) + hasVariableCountThat().isEqualTo(0) + } + + val poly31 = parseAlgebraicExpressionWithAllErrors("4*(x+2)").toPolynomial() + assertThat(poly31).evaluatesToPlainTextThat().isEqualTo("4x + 8") + assertThat(poly31).hasTermCountThat().isEqualTo(2) + assertThat(poly31).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(4) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly31).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(8) + hasVariableCountThat().isEqualTo(0) + } + + val poly7 = parseAlgebraicExpressionWithAllErrors("2xy^2z^3").toPolynomial() + assertThat(poly7).evaluatesToPlainTextThat().isEqualTo("2xy^2z^3") + assertThat(poly7).hasTermCountThat().isEqualTo(1) + assertThat(poly7).term(0).hasVariableCountThat().isEqualTo(3) + assertThat(poly7).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) + assertThat(poly7).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly7).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly7).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly7).term(0).variable(1).hasPowerThat().isEqualTo(2) + assertThat(poly7).term(0).variable(2).hasNameThat().isEqualTo("z") + assertThat(poly7).term(0).variable(2).hasPowerThat().isEqualTo(3) + + // Show that 7+xy+yz-3-xz-yz+3xy-4 combines into 4xy-xz (the eliminated terms should be gone). + val poly8 = parseAlgebraicExpressionWithAllErrors("xy+yz-xz-yz+3xy").toPolynomial() + assertThat(poly8).evaluatesToPlainTextThat().isEqualTo("4xy - xz") + assertThat(poly8).hasTermCountThat().isEqualTo(2) + assertThat(poly8).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly8).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(4) + assertThat(poly8).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly8).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly8).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly8).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly8).term(1).hasVariableCountThat().isEqualTo(2) + assertThat(poly8).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(-1) + assertThat(poly8).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly8).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly8).term(1).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly8).term(1).variable(1).hasPowerThat().isEqualTo(1) + + // x+2x should become 3x since like terms are combined. + val poly9 = parseAlgebraicExpressionWithAllErrors("x+2x").toPolynomial() + assertThat(poly9).evaluatesToPlainTextThat().isEqualTo("3x") + assertThat(poly9).hasTermCountThat().isEqualTo(1) + assertThat(poly9).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly9).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(3) + assertThat(poly9).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly9).term(0).variable(0).hasPowerThat().isEqualTo(1) + + // xx^2 should become x^3 since like terms are combined. + val poly10 = parseAlgebraicExpressionWithAllErrors("xx^2").toPolynomial() + assertThat(poly10).evaluatesToPlainTextThat().isEqualTo("x^3") + assertThat(poly10).hasTermCountThat().isEqualTo(1) + assertThat(poly10).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly10).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly10).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly10).term(0).variable(0).hasPowerThat().isEqualTo(3) + + // No terms in this polynomial should be combined. + val poly11 = parseAlgebraicExpressionWithAllErrors("x^2+x+1").toPolynomial() + assertThat(poly11).evaluatesToPlainTextThat().isEqualTo("x^2 + x + 1") + assertThat(poly11).hasTermCountThat().isEqualTo(3) + assertThat(poly11).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly11).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly11).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly11).term(0).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly11).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly11).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly11).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly11).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly11).term(2).hasVariableCountThat().isEqualTo(0) + assertThat(poly11).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + + // No terms in this polynomial should be combined. + val poly12 = parseAlgebraicExpressionWithAllErrors("x^2 + x^2y").toPolynomial() + assertThat(poly12).evaluatesToPlainTextThat().isEqualTo("x^2y + x^2") + assertThat(poly12).hasTermCountThat().isEqualTo(2) + assertThat(poly12).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly12).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly12).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly12).term(0).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly12).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly12).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly12).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly12).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly12).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly12).term(1).variable(0).hasPowerThat().isEqualTo(2) + + // Ordering tests. Verify that ordering matches + // https://en.wikipedia.org/wiki/Polynomial#Definition (where multiple variables are sorted + // lexicographically). + + // The order of the terms in this polynomial should be reversed. + val poly14 = parseAlgebraicExpressionWithAllErrors("1+x+x^2+x^3").toPolynomial() + assertThat(poly14).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") + assertThat(poly14).hasTermCountThat().isEqualTo(4) + assertThat(poly14).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly14).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly14).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly14).term(0).variable(0).hasPowerThat().isEqualTo(3) + assertThat(poly14).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly14).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly14).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly14).term(1).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly14).term(2).hasVariableCountThat().isEqualTo(1) + assertThat(poly14).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly14).term(2).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly14).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly14).term(3).hasVariableCountThat().isEqualTo(0) + assertThat(poly14).term(3).hasCoefficientThat().isIntegerThat().isEqualTo(1) + + // The order of the terms in this polynomial should be preserved. + val poly15 = parseAlgebraicExpressionWithAllErrors("x^3+x^2+x+1").toPolynomial() + assertThat(poly15).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") + assertThat(poly15).hasTermCountThat().isEqualTo(4) + assertThat(poly15).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly15).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly15).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly15).term(0).variable(0).hasPowerThat().isEqualTo(3) + assertThat(poly15).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly15).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly15).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly15).term(1).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly15).term(2).hasVariableCountThat().isEqualTo(1) + assertThat(poly15).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly15).term(2).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly15).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly15).term(3).hasVariableCountThat().isEqualTo(0) + assertThat(poly15).term(3).hasCoefficientThat().isIntegerThat().isEqualTo(1) + + // The order of the terms in this polynomial should be reversed. + val poly16 = parseAlgebraicExpressionWithAllErrors("xy+xz+yz").toPolynomial() + assertThat(poly16).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") + assertThat(poly16).hasTermCountThat().isEqualTo(3) + assertThat(poly16).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly16).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly16).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly16).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly16).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(1).hasVariableCountThat().isEqualTo(2) + assertThat(poly16).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly16).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly16).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(1).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly16).term(1).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(2).hasVariableCountThat().isEqualTo(2) + assertThat(poly16).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly16).term(2).variable(0).hasNameThat().isEqualTo("y") + assertThat(poly16).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(2).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly16).term(2).variable(1).hasPowerThat().isEqualTo(1) + + // The order of the terms in this polynomial should be preserved. + val poly17 = parseAlgebraicExpressionWithAllErrors("yz+xz+xy").toPolynomial() + assertThat(poly17).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") + assertThat(poly17).hasTermCountThat().isEqualTo(3) + assertThat(poly17).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly17).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly17).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly17).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly17).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(1).hasVariableCountThat().isEqualTo(2) + assertThat(poly17).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly17).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly17).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(1).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly17).term(1).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(2).hasVariableCountThat().isEqualTo(2) + assertThat(poly17).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly17).term(2).variable(0).hasNameThat().isEqualTo("y") + assertThat(poly17).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(2).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly17).term(2).variable(1).hasPowerThat().isEqualTo(1) + + val poly18 = parseAlgebraicExpressionWithAllErrors("3+x+y+xy+x^2y+xy^2+x^2y^2").toPolynomial() + assertThat(poly18).evaluatesToPlainTextThat().isEqualTo("x^2y^2 + x^2y + xy^2 + xy + x + y + 3") + assertThat(poly18).hasTermCountThat().isEqualTo(7) + assertThat(poly18).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly18).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly18).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(4).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(5).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(6).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(0) + } + + // Ensure variables of coefficient and power of 0 are removed. + val poly22 = parseAlgebraicExpressionWithAllErrors("0x").toPolynomial() + assertThat(poly22).evaluatesToPlainTextThat().isEqualTo("0") + assertThat(poly22).hasTermCountThat().isEqualTo(1) + assertThat(poly22).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(0) + hasVariableCountThat().isEqualTo(0) + } + + val poly23 = parseAlgebraicExpressionWithAllErrors("x-x").toPolynomial() + assertThat(poly23).evaluatesToPlainTextThat().isEqualTo("0") + assertThat(poly23).hasTermCountThat().isEqualTo(1) + assertThat(poly23).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(0) + hasVariableCountThat().isEqualTo(0) + } + + val poly24 = parseAlgebraicExpressionWithAllErrors("x^0").toPolynomial() + assertThat(poly24).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly24).hasTermCountThat().isEqualTo(1) + assertThat(poly24).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + + val poly25 = parseAlgebraicExpressionWithAllErrors("x/x").toPolynomial() + assertThat(poly25).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly25).hasTermCountThat().isEqualTo(1) + assertThat(poly25).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + + val poly26 = parseAlgebraicExpressionWithAllErrors("x^(2-2)").toPolynomial() + assertThat(poly26).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly26).hasTermCountThat().isEqualTo(1) + assertThat(poly26).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + + val poly28 = parseAlgebraicExpressionWithAllErrors("(x+1)/2").toPolynomial() + assertThat(poly28).evaluatesToPlainTextThat().isEqualTo("(1/2)x + 1/2") + assertThat(poly28).hasTermCountThat().isEqualTo(2) + assertThat(poly28).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly28).term(1).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(0) + } + + // Ensure like terms are combined after polynomial multiplication. + val poly20 = parseAlgebraicExpressionWithAllErrors("(x-5)(x+2)").toPolynomial() + assertThat(poly20).evaluatesToPlainTextThat().isEqualTo("x^2 - 3x - 10") + assertThat(poly20).hasTermCountThat().isEqualTo(3) + assertThat(poly20).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly20).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly20).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-10) + hasVariableCountThat().isEqualTo(0) + } + + val poly21 = parseAlgebraicExpressionWithAllErrors("(1+x)^3").toPolynomial() + assertThat(poly21).evaluatesToPlainTextThat().isEqualTo("x^3 + 3x^2 + 3x + 1") + assertThat(poly21).hasTermCountThat().isEqualTo(4) + assertThat(poly21).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + assertThat(poly21).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly21).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly21).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + + val poly27 = parseAlgebraicExpressionWithAllErrors("x^2*y^2 + 2").toPolynomial() + assertThat(poly27).evaluatesToPlainTextThat().isEqualTo("x^2y^2 + 2") + assertThat(poly27).hasTermCountThat().isEqualTo(2) + assertThat(poly27).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly27).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(0) + } + + val poly32 = parseAlgebraicExpressionWithAllErrors("(x^2-3*x-10)*(x+2)").toPolynomial() + assertThat(poly32).evaluatesToPlainTextThat().isEqualTo("x^3 - x^2 - 16x - 20") + assertThat(poly32).hasTermCountThat().isEqualTo(4) + assertThat(poly32).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + assertThat(poly32).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly32).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-16) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly32).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-20) + hasVariableCountThat().isEqualTo(0) + } + + val poly33 = parseAlgebraicExpressionWithAllErrors("(x-y)^3").toPolynomial() + assertThat(poly33).evaluatesToPlainTextThat().isEqualTo("x^3 - 3x^2y + 3xy^2 - y^3") + assertThat(poly33).hasTermCountThat().isEqualTo(4) + assertThat(poly33).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + assertThat(poly33).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly33).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly33).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(3) + } + } + + // Ensure polynomial division works. + val poly19 = parseAlgebraicExpressionWithAllErrors("(x^2-3*x-10)/(x+2)").toPolynomial() + assertThat(poly19).evaluatesToPlainTextThat().isEqualTo("x - 5") + assertThat(poly19).hasTermCountThat().isEqualTo(2) + assertThat(poly19).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly19).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-5) + hasVariableCountThat().isEqualTo(0) + } + + val poly35 = parseAlgebraicExpressionWithAllErrors("(xy-5y)/y").toPolynomial() + assertThat(poly35).evaluatesToPlainTextThat().isEqualTo("x - 5") + assertThat(poly35).hasTermCountThat().isEqualTo(2) + assertThat(poly35).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly35).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-5) + hasVariableCountThat().isEqualTo(0) + } + + val poly36 = parseAlgebraicExpressionWithAllErrors("(x^2-2xy+y^2)/(x-y)").toPolynomial() + assertThat(poly36).evaluatesToPlainTextThat().isEqualTo("x - y") + assertThat(poly36).hasTermCountThat().isEqualTo(2) + assertThat(poly36).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly36).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + + // Example from https://www.kristakingmath.com/blog/predator-prey-systems-ghtcp-5e2r4-427ab. + val poly37 = parseAlgebraicExpressionWithAllErrors("(x^3-y^3)/(x-y)").toPolynomial() + assertThat(poly37).evaluatesToPlainTextThat().isEqualTo("x^2 + xy + y^2") + assertThat(poly37).hasTermCountThat().isEqualTo(3) + assertThat(poly37).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly37).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly37).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + + // Multi-variable & more complex division. + val poly34 = + parseAlgebraicExpressionWithAllErrors("(x^3-3x^2y+3xy^2-y^3)/(x-y)^2").toPolynomial() + assertThat(poly34).evaluatesToPlainTextThat().isEqualTo("x - y") + assertThat(poly34).hasTermCountThat().isEqualTo(2) + assertThat(poly34).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly34).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + + val poly38 = parseNumericExpressionWithAllErrors("2^-4").toPolynomial() + assertThat(poly38).evaluatesToPlainTextThat().isEqualTo("1/16") + assertThat(poly38).hasTermCountThat().isEqualTo(1) + assertThat(poly38).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(16) + } + hasVariableCountThat().isEqualTo(0) + } + + val poly39 = parseNumericExpressionWithAllErrors("2^(3-6)").toPolynomial() + assertThat(poly39).evaluatesToPlainTextThat().isEqualTo("1/8") + assertThat(poly39).hasTermCountThat().isEqualTo(1) + assertThat(poly39).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(8) + } + hasVariableCountThat().isEqualTo(0) + } + + // x^-3 is not a valid polynomial (since polynomials can't have negative powers). + val poly40 = parseAlgebraicExpressionWithAllErrors("x^(3-6)").toPolynomial() + assertThat(poly40).isNotValidPolynomial() + + // 2^x is not a polynomial. + val poly41 = parseAlgebraicExpressionWithoutOptionalErrors("2^x").toPolynomial() + assertThat(poly41).isNotValidPolynomial() + + // 1/x is not a polynomial. + val poly42 = parseAlgebraicExpressionWithoutOptionalErrors("1/x").toPolynomial() + assertThat(poly42).isNotValidPolynomial() + + val poly43 = parseAlgebraicExpressionWithAllErrors("x/2").toPolynomial() + assertThat(poly43).evaluatesToPlainTextThat().isEqualTo("(1/2)x") + assertThat(poly43).hasTermCountThat().isEqualTo(1) + assertThat(poly43).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + + val poly44 = parseAlgebraicExpressionWithAllErrors("(x-3)/2").toPolynomial() + assertThat(poly44).evaluatesToPlainTextThat().isEqualTo("(1/2)x - 3/2") + assertThat(poly44).hasTermCountThat().isEqualTo(2) + assertThat(poly44).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly44).term(1).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isTrue() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(3) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(0) + } + + val poly45 = parseAlgebraicExpressionWithAllErrors("(x-1)(x+1)").toPolynomial() + assertThat(poly45).evaluatesToPlainTextThat().isEqualTo("x^2 - 1") + assertThat(poly45).hasTermCountThat().isEqualTo(2) + assertThat(poly45).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly45).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + + // √x is not a polynomial. + val poly46 = parseAlgebraicExpressionWithAllErrors("sqrt(x)").toPolynomial() + assertThat(poly46).isNotValidPolynomial() + + val poly47 = parseAlgebraicExpressionWithAllErrors("√(x^2)").toPolynomial() + assertThat(poly47).evaluatesToPlainTextThat().isEqualTo("x") + assertThat(poly47).hasTermCountThat().isEqualTo(1) + assertThat(poly47).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + + val poly51 = parseAlgebraicExpressionWithAllErrors("√(x^2y^2)").toPolynomial() + assertThat(poly51).evaluatesToPlainTextThat().isEqualTo("xy") + assertThat(poly51).hasTermCountThat().isEqualTo(1) + assertThat(poly51).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + + // A limitation in the current polynomial conversion is that sqrt(x) will fail due to it not + // have any polynomial representation. + val poly48 = parseAlgebraicExpressionWithAllErrors("√x^2").toPolynomial() + assertThat(poly48).isNotValidPolynomial() + + // √(x^2+2) may evaluate to a polynomial, but it requires factoring (which isn't yet supported). + val poly50 = parseAlgebraicExpressionWithAllErrors("√(x^2+2)").toPolynomial() + assertThat(poly50).isNotValidPolynomial() + + // Division by zero is undefined, so a polynomial can't be constructed. + val poly49 = parseAlgebraicExpressionWithoutOptionalErrors("(x+2)/0").toPolynomial() + assertThat(poly49).isNotValidPolynomial() + + val poly52 = parsePolynomialFromNumericExpression("1") + val poly53 = parsePolynomialFromNumericExpression("0") + assertThat(poly52).isNotEqualTo(poly53) + + val poly54 = parsePolynomialFromNumericExpression("1+2") + val poly55 = parsePolynomialFromNumericExpression("3") + assertThat(poly54).isEqualTo(poly55) + + val poly56 = parsePolynomialFromNumericExpression("1-2") + val poly57 = parsePolynomialFromNumericExpression("-1") + assertThat(poly56).isEqualTo(poly57) + + val poly58 = parsePolynomialFromNumericExpression("2*3") + val poly59 = parsePolynomialFromNumericExpression("6") + assertThat(poly58).isEqualTo(poly59) + + val poly60 = parsePolynomialFromNumericExpression("2^3") + val poly61 = parsePolynomialFromNumericExpression("8") + assertThat(poly60).isEqualTo(poly61) + + val poly62 = parsePolynomialFromAlgebraicExpression("1+x") + val poly63 = parsePolynomialFromAlgebraicExpression("x+1") + assertThat(poly62).isEqualTo(poly63) + + val poly64 = parsePolynomialFromAlgebraicExpression("y+x") + val poly65 = parsePolynomialFromAlgebraicExpression("x+y") + assertThat(poly64).isEqualTo(poly65) + + val poly66 = parsePolynomialFromAlgebraicExpression("(x+1)^2") + val poly67 = parsePolynomialFromAlgebraicExpression("x^2+2x+1") + assertThat(poly66).isEqualTo(poly67) + + val poly68 = parsePolynomialFromAlgebraicExpression("(x+1)/2") + val poly69 = parsePolynomialFromAlgebraicExpression("x/2+(1/2)") + assertThat(poly68).isEqualTo(poly69) + + val poly70 = parsePolynomialFromAlgebraicExpression("x*2") + val poly71 = parsePolynomialFromAlgebraicExpression("2x") + assertThat(poly70).isEqualTo(poly71) + + val poly72 = parsePolynomialFromAlgebraicExpression("x(x+1)") + val poly73 = parsePolynomialFromAlgebraicExpression("x^2+x") + assertThat(poly72).isEqualTo(poly73) + } + private fun createComparableOperationListFromNumericExpression(expression: String) = parseNumericExpressionWithAllErrors(expression).toComparableOperationList() private fun createComparableOperationListFromAlgebraicExpression(expression: String) = parseAlgebraicExpressionWithAllErrors(expression).toComparableOperationList() + private fun parsePolynomialFromNumericExpression(expression: String) = + parseNumericExpressionWithAllErrors(expression).toPolynomial() + + private fun parsePolynomialFromAlgebraicExpression(expression: String) = + parseAlgebraicExpressionWithAllErrors(expression).toPolynomial() + @DslMarker private annotation class ExpressionComparatorMarker @DslMarker private annotation class ComparableOperationComparatorMarker @@ -6703,6 +7579,58 @@ class NumericExpressionParserTest { } } + private class PolynomialSubject( + metadata: FailureMetadata, + private val actual: Polynomial? + ) : LiteProtoSubject(metadata, actual) { + private val nonNullActual by lazy { + checkNotNull(actual) { + "Expected polynomial to be defined, not null (is the expression/equation not a valid" + + " polynomial?)" + } + } + + fun isNotValidPolynomial() { + assertWithMessage( + "Expected polynomial to be undefined, but was: ${actual?.toPlainText()}" + ).that(actual).isNull() + } + + fun isConstantThat(): RealSubject { + assertWithMessage("Expected polynomial to be constant: $nonNullActual") + .that(nonNullActual.isConstant()) + .isTrue() + return assertThat(nonNullActual.getConstant()) + } + + fun hasTermCountThat(): IntegerSubject = assertThat(nonNullActual.termCount) + + fun term(index: Int): PolynomialTermSubject = assertThat(nonNullActual.termList[index]) + + fun evaluatesToPlainTextThat(): StringSubject = assertThat(nonNullActual.toPlainText()) + } + + private class PolynomialTermSubject( + metadata: FailureMetadata, + private val actual: Polynomial.Term + ) : LiteProtoSubject(metadata, actual) { + fun hasCoefficientThat(): RealSubject = assertThat(actual.coefficient) + + fun hasVariableCountThat(): IntegerSubject = assertThat(actual.variableCount) + + fun variable(index: Int): PolynomialTermVariableSubject = + assertThat(actual.variableList[index]) + } + + private class PolynomialTermVariableSubject( + metadata: FailureMetadata, + private val actual: Polynomial.Term.Variable + ) : LiteProtoSubject(metadata, actual) { + fun hasNameThat(): StringSubject = assertThat(actual.name) + + fun hasPowerThat(): IntegerSubject = assertThat(actual.power) + } + private companion object { // TODO: fix helper API. @@ -6796,5 +7724,14 @@ class NumericExpressionParserTest { private fun assertThat(actual: ComparableOperationList): ComparableOperationListSubject = assertAbout(::ComparableOperationListSubject).that(actual) + + private fun assertThat(actual: Polynomial?): PolynomialSubject = + assertAbout(::PolynomialSubject).that(actual) + + private fun assertThat(actual: Polynomial.Term): PolynomialTermSubject = + assertAbout(::PolynomialTermSubject).that(actual) + + private fun assertThat(actual: Polynomial.Term.Variable): PolynomialTermVariableSubject = + assertAbout(::PolynomialTermVariableSubject).that(actual) } } From 56c788c55ca9cea5ab6405f3d836f0a9a1b3a971 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 13 Dec 2021 19:05:15 -0800 Subject: [PATCH 089/289] Delete old code. Also, fix broken tokenizer test. --- .../org/oppia/android/util/math/BUILD.bazel | 15 - .../android/util/math/FloatExtensions.kt | 2 +- .../util/math/MathExpressionExtensions.kt | 150 +- .../android/util/math/MathExpressionParser.kt | 1445 ++- .../oppia/android/util/math/MathTokenizer.kt | 760 +- .../oppia/android/util/math/MathTokenizer2.kt | 359 - .../util/math/NumericExpressionParser.kt | 1038 -- .../org/oppia/android/util/math/BUILD.bazel | 22 +- .../util/math/MathExpressionParserTest.kt | 8603 +++++++++++++++-- .../android/util/math/MathTokenizerTest.kt | 564 +- .../util/math/NumericExpressionParserTest.kt | 7737 --------------- 11 files changed, 8899 insertions(+), 11796 deletions(-) delete mode 100644 utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt delete mode 100644 utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt delete mode 100644 utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 56f1470dbae..c633d3b1763 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -28,20 +28,6 @@ kt_android_library( visibility = [ "//:oppia_testing_visibility", ], - deps = [ - ":tokenizer", - "//model/src/main/proto:math_java_proto_lite", - ], -) - -kt_android_library( - name = "numeric_expression_parser", - srcs = [ - "NumericExpressionParser.kt", - ], - visibility = [ - "//:oppia_testing_visibility", - ], deps = [ ":extensions", ":math_parsing_error", @@ -55,7 +41,6 @@ kt_android_library( name = "tokenizer", srcs = [ "MathTokenizer.kt", - "MathTokenizer2.kt", ], visibility = [ "//:oppia_testing_visibility", diff --git a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt index 8d309a67999..62504046c78 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt @@ -3,7 +3,7 @@ package org.oppia.android.util.math import kotlin.math.abs /** The error margin used for float equality by [Float.approximatelyEquals]. */ -public const val FLOAT_EQUALITY_INTERVAL = 1e-5 +const val FLOAT_EQUALITY_INTERVAL = 1e-5 /** Returns whether this float approximately equals another based on a consistent epsilon value. */ fun Float.approximatelyEquals(other: Float): Boolean { diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 6f73b8a99af..147e074cd94 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -53,18 +53,6 @@ import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED // TODO: split up this extensions file into multiple, clean it up, reorganize, and add tests. -// XXX: Polynomials can actually take lots of forms, so this conversion is intentionally limited -// based on intended use cases and can be extended in the future, as needed. For example, both -// x^(2^3) and ((x^2+2x+1)/(x+1)) won't be considered polynomials, but it can be seen that they are -// indeed after being simplified. This implementation doesn't yet support recognizing binomials -// (e.g. (x+1)^2) but needs to be updated to. The representation for Polynomial doesn't quite -// support much outside of general form (except negative exponents). - -// TODO: Consider expressions like x/2 (should be treated like (1/2)x). -// TODO: Consider: (1+2)*x or (1+2)x -// TODO: Make sure that all 'when' cases here do not use 'else' branches to ensure structural -// changes require changing logic. - private val COMPARABLE_OPERATION_COMPARATOR: Comparator by lazy { // Some of the comparators must be deferred since they indirectly reference this comparator (which // isn't valid until it's fully assembled). @@ -649,7 +637,6 @@ fun MathExpression.toRawLatex(divAsFraction: Boolean): String { } } -// TODO: add proper error channels for the return value. fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() fun MathExpression.evaluate(): Real? { @@ -701,55 +688,14 @@ private fun MathFunctionCall.evaluate(): Real? { } } -// Also: consider how to perform expression analysis for non-polynomial trees - fun MathExpression.toPolynomial(): Polynomial? { - // Constructing a polynomial more or less requires: - // 1. Collecting all variables (these will either be part of exponent expressions if they have a - // power, part of a multiplication expression with a constant/evaluable constant directly or once - // removed, or part of another expression) and evaluating them into terms. - // 2. Collecting all non-variable values as top-level constant terms. - // Note that polynomials are always representable via summation, so multiplication, division, and - // subtraction must be partially evaluated and eliminated (if they can't be eliminated then the - // expression is not polynomial or requires a more complex algorithm for reduction such as - // polynomial long division). - - // ----- Algo probably requires additional data structure component since it's changing the tree piece-by-piece. - // Consider having two versions of the conversion: one for rigid polynomials and another for forcing expressions into a polynomial. - - // First thoughts on a possible algorithm: - // 1. Find all variable expressions, then go up 1 node (short-circuit: if no parent, it's a polynomial with 1 variable term) - // 2. For each variable: - // a. If the parent is a multiplication expression, try to reduce the other term to a constant (this would be the term of the variable). Remove the multiplication term. - // b. If the parent is an exponent, try to reduce the right-hand side. This would become the variable's power. Remove the exponent term. - // c. If the parent is a unary operation, in-line that. - // 3. Repeat (2) until variables become irreducible. - // 4. Enumerate all exponents, multiplications, and divisions: reduce each at its parent to a constant. - // 5. Replace remaining subtractions with additions + unary operators, then reduce all unary operations to constants. - // 6. Check: there should be no remaining exponents, multiplications, divisions, subtractions, or unary operations (only addition should remain). If there are, fail: this isn't a polynomial or it isn't one that we support. - // 7. Traverse the tree and convert each addition operand into a term and construct the final polynomial. - // 8. Optional: further reduce the polynomial and/or convert to general form. - - // Consider revising the above to recursively find nested polynomials and "build them up". This - // will allow us to detect each of the pathological cases that can't be handled by the above, plus - // trivial cases the are handled by the above: - // 1. Top-level polynomial / polynomials being added / polynomials being subtracted (should be handled by the above algo) - // 2. Polynomial being divided by another polynomial (we should just fail here--it's quite complex to solve this) - // 3. Polynomial raised by a constant positive whole number power (e.g. binomial); any other exponent isn't a polynomial (including a polynomial exp) - // 4. Polynomials multiplied by each other (requires expanding like #3, probably via matrix multiplication - // 5. Combinations of 1-4 (which requires recursion to find a complete solution for) - - // Final algorithm (non-simplified): - // 1. Copy the tree so it can be augmented as nodes(expression | polynomial | constant) - // 2. Replace all variable expressions with polynomials - // 3. Depth-first evaluate the entire graph (results are polynomials OR concatenation). If any fails, this is not a polynomial or is unsupported. Specifics: - // a. Unary (apply to coefficients of the terms) - // b. Exponents (right-hand side must be reducible to constant -> calculate the power); may require expansion (e.g. for binomials) - // c. Subtraction (replace with addition & negate the coefficient of the polynomial terms) - // d. Division (right-hand side must be reducible to constant -> apply to the term, e.g. for x/4) - // e. Multiplication (for one side constant, apply to coefficients otherwise perform polynomial multiplication) - // f. Addition (treat constants as constant terms & concatenate term lists to compute new polynomial) - // 4. Collect the final polynomial as the result. Early exiting indicates a non-polynomial. + // Polynomials are created by converting subsequent parts of a math expression to polynomials and + // then combining them using math operations. In some cases (such as exponentiation and division), + // this can fail (which indicates a non-polynomial expression). In other cases (such as square + // roots), additional processing is needed. This method guarantees that there are no zero terms + // returned, and that the returned polynomial's terms are deterministically arranged such that two + // similar expressions only different by commutativity/term reordering will result in exactly the + // same polynomial (i.e. Polynomial.isEqualTo method will return true when comparing the two). return stripGroups().replaceSquareRoots() .reduceToPolynomial() ?.removeUnnecessaryVariables() @@ -760,14 +706,6 @@ private fun Polynomial.sort() = Polynomial.newBuilder().apply { addAllTerm(this@sort.termList.sortedWith(POLYNOMIAL_TERM_COMPARATOR)) }.build() -fun Polynomial.isUnivariate(): Boolean = getUniqueVariableCount() == 1 - -fun Polynomial.isMultivariate(): Boolean = getUniqueVariableCount() > 1 - -private fun Polynomial.getUniqueVariableCount(): Int { - return termList.flatMap(Term::getVariableList).map(Variable::getName).toSet().size -} - fun Polynomial.toPlainText(): String { return termList.map { it.toPlainText() }.reduce { acc, termAnswerStr -> if (termAnswerStr.startsWith("-")) { @@ -869,7 +807,6 @@ private operator fun Polynomial.unaryMinus(): Polynomial { .build() } -// TODO: extract the filtering done during addition & also do it at the end in case initial polynomials are tried (like x^0, or 0y). Add tests for these cases. private operator fun Polynomial.plus(rhs: Polynomial): Polynomial { // Adding two polynomials just requires combining their terms lists (taking into account combining // common terms). @@ -920,8 +857,6 @@ private operator fun Term.times(rhs: Term): Term { } private operator fun Polynomial.div(rhs: Polynomial): Polynomial? { - // TODO: ensure this properly computes distributions for fractions, e.g. ((x+3)/2) should become - // (1/2)x + (3/2). // See https://en.wikipedia.org/wiki/Polynomial_long_division#Pseudocode for reference. if (rhs.isApproximatelyZero()) { return null // Dividing by zero is invalid and thus cannot yield a polynomial. @@ -1130,24 +1065,6 @@ private fun Polynomial.pow(exp: Polynomial): Polynomial? { private fun Polynomial.isSingleTerm(): Boolean = termList.size == 1 -//private fun MathExpression.toTreeNode(): ExpressionTreeNode { -// return when (expressionTypeCase) { -// CONSTANT -> ExpressionTreeNode.ConstantNode(constant) -// VARIABLE -> ExpressionTreeNode.PolynomialNode(createSingleTermPolynomial(variable)) -// UNARY_OPERATION -> ExpressionTreeNode.ExpressionNode(this, unaryOperation.collectChildren()) -// BINARY_OPERATION -> ExpressionTreeNode.ExpressionNode(this, binaryOperation.collectChildren()) -// else -> ExpressionTreeNode.ExpressionNode(this, mutableListOf()) -// } -//} -// -//private fun MathUnaryOperation.collectChildren(): MutableList { -// return mutableListOf(operand.toTreeNode()) -//} -// -//private fun MathBinaryOperation.collectChildren(): MutableList { -// return mutableListOf(leftOperand.toTreeNode(), rightOperand.toTreeNode()) -//} - private fun createSingleVariablePolynomial(variableName: String): Polynomial { return createSingleTermPolynomial( Term.newBuilder().apply { @@ -1178,59 +1095,6 @@ private fun createZeroTerm() = Term.newBuilder().apply { coefficient = createCoefficientValueOfZero() }.build() -private sealed class ExpressionTreeNode { - data class ExpressionNode( - val mathExpression: MathExpression, - val children: MutableList - ) : ExpressionTreeNode() - - data class PolynomialNode(val polynomial: Polynomial) : ExpressionTreeNode() - - data class ConstantNode(val constant: Real) : ExpressionTreeNode() -} - -// TODO: add a faster isReducibleToConstant recursive function since this is used a lot. - -// private fun MathExpression.reduceToConstant(): MathExpression? { -// return when (expressionTypeCase) { -// CONSTANT -> this -// VARIABLE -> null -// UNARY_OPERATION -> unaryOperation.reduceToConstant() -// BINARY_OPERATION -> binaryOperation.reduceToConstant() -// else -> null -// } -// } - -// private fun MathUnaryOperation.reduceToConstant(): MathExpression? { -// return when (operator) { -// MathUnaryOperation.Operator.NEGATE -> operand.reduceToConstant()?.transformConstant { -it } -// else -> null -// } -// } - -// private fun MathBinaryOperation.reduceToConstant(): MathExpression? { -// val leftConstant = leftOperand.reduceToConstant()?.constant ?: return null -// val rightConstant = rightOperand.reduceToConstant()?.constant ?: return null -// return when (operator) { -// MathBinaryOperation.Operator.ADD -> fromConstant(leftConstant + rightConstant) -// MathBinaryOperation.Operator.SUBTRACT -> fromConstant(leftConstant - rightConstant) -// MathBinaryOperation.Operator.MULTIPLY -> fromConstant(leftConstant * rightConstant) -// MathBinaryOperation.Operator.DIVIDE -> fromConstant(leftConstant / rightConstant) -// MathBinaryOperation.Operator.EXPONENTIATE -> fromConstant(leftConstant.pow(rightConstant)) -// else -> null -// } -// } - -private fun MathExpression.transformConstant( - transform: (Real.Builder) -> Real.Builder -): MathExpression { - return toBuilder().setConstant(transform(constant.toBuilder())).build() -} - -private fun fromConstant(real: Real): MathExpression { - return MathExpression.newBuilder().setConstant(real).build() -} - private fun Real.isApproximatelyEqualTo(value: Double): Boolean { return toDouble().approximatelyEquals(value) } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index beb8190ed66..90447df7532 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -1,548 +1,1039 @@ package org.oppia.android.util.math -import org.oppia.android.app.model.Fraction +import kotlin.math.absoluteValue import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE import org.oppia.android.app.model.Real -import org.oppia.android.util.math.MathTokenizer.Token.DecimalNumber -import org.oppia.android.util.math.MathTokenizer.Token.Identifier -import org.oppia.android.util.math.MathTokenizer.Token.InvalidIdentifier -import org.oppia.android.util.math.MathTokenizer.Token.InvalidToken -import org.oppia.android.util.math.MathTokenizer.Token.OpenParenthesis -import org.oppia.android.util.math.MathTokenizer.Token.Operator -import org.oppia.android.util.math.MathTokenizer.Token.WholeNumber -import java.util.ArrayDeque -import java.util.Stack - -/** - * Contains functionality for parsing mathematical expressions, including both numeric and - * polynomial-based algebraic expressions (functions are not currently supported). - */ -class MathExpressionParser { - companion object { - /** The result of parsing an expression. See the subclasses for the different possibilities. */ - sealed class ParseResult { - /** Indicates a successful parse with a corresponding [MathExpression]. */ - data class Success(val mathExpression: MathExpression) : ParseResult() +import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError +import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError +import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError +import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError +import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError +import org.oppia.android.util.math.MathParsingError.FunctionNameIncompleteError +import org.oppia.android.util.math.MathParsingError.GenericError +import org.oppia.android.util.math.MathParsingError.HangingSquareRootError +import org.oppia.android.util.math.MathParsingError.InvalidFunctionInUseError +import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.NestedExponentsError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberAfterBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberBeforeBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NumberAfterVariableError +import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError +import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError +import org.oppia.android.util.math.MathParsingError.SubsequentBinaryOperatorsError +import org.oppia.android.util.math.MathParsingError.SubsequentUnaryOperatorsError +import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError +import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError +import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError +import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError +import org.oppia.android.util.math.MathTokenizer.Companion.Token +import org.oppia.android.util.math.MathTokenizer.Companion.Token.DivideSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.EqualsSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.ExponentiationSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.FunctionName +import org.oppia.android.util.math.MathTokenizer.Companion.Token.IncompleteFunctionName +import org.oppia.android.util.math.MathTokenizer.Companion.Token.InvalidToken +import org.oppia.android.util.math.MathTokenizer.Companion.Token.LeftParenthesisSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.MinusSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.MultiplySymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.PlusSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.PositiveInteger +import org.oppia.android.util.math.MathTokenizer.Companion.Token.PositiveRealNumber +import org.oppia.android.util.math.MathTokenizer.Companion.Token.RightParenthesisSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.SquareRootSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.VariableName +import org.oppia.android.util.math.MathExpressionParser.ParseContext.AlgebraicExpressionContext +import org.oppia.android.util.math.MathExpressionParser.ParseContext.NumericExpressionContext +import org.oppia.android.util.math.MathTokenizer.Companion.BinaryOperatorToken + +class MathExpressionParser private constructor(private val parseContext: ParseContext) { + // TODO: + // - Add helpers to reduce overall parser length. + // - Integrate with new errors & update the routines to not rely on exceptions except in actual exceptional cases. Make sure optional errors can be disabled (for testing purposes). + // - Rename this to be a generic parser, update the public API, add documentation, remove the old classes, and split up the big test routines into actual separate tests. + + // TODO: implement specific errors. + // TODO: verify remaining GenericErrors are correct. + + // TODO: document that 'generic' means either 'numeric' or 'algebraic' (ie that the expression is syntactically the same between both grammars). + // TODO: document that one design goal is keeping the grammar for this parser as LL(1) & why. - // TODO(BenHenning): Replace this with an enum so that UI code can show a reasonable error to - // the user. - /** Indicates the parse failed with a developer-readable string. */ - data class Failure(val failureReason: String) : ParseResult() + private fun parseGenericEquationGrammar(): MathParsingResult { + // generic_equation_grammar = generic_equation ; + return parseGenericEquation().maybeFail { equation -> + checkForLearnerErrors(equation.leftSide) ?: checkForLearnerErrors(equation.rightSide) } + } - /** - * Parses the specified raw expression literal with the list of allowed variables and returns a - * [ParseResult] with either the parsed expression tree or a failure if the expression has an - * error. - * - * Note that this parsing will include some cases of implied multiplication. For example, each - * of the following cases will result in a valid parse with a multiplication operator despite - * one not being explicitly present in the expression: - * - 2x -> 2 * x (note that '2 x' will have the same result) - * - x2 -> x * 2 - * - (x+1)(x+2) -> (x+1) * (x+2) (note that whitespace between the groups is ignored) - * - xy -> x * y (note that 'x y' will have the same result) - * - x(x+1) -> x * (x+1) - * - 2(x+1) -> 2 * (x+1) - * - (x+1)x -> (x+1) * x - * - (x+1)2 -> (x+1) * 2 - * - * No other cases will result in implied multiplication (including '2 2' which is an invalid - * expression). However, sometimes these invalid cases can be made valid (e.g. (2)(3) should - * result in a proper, implied multiplication scenario since this is treated as polynomial - * multiplication). Note also that long variables (e.g. 'lambda' will be treated the same except - * they can never have implied multiplication with other variables since the parser cannot - * reasonably distinguish between different variables that are more than one letter long). - */ - fun parseExpression(literalExpression: String, allowedVariables: List): ParseResult { - // An implementation of the Shunting Yard algorithm adapted to support variables, different - // number types, and the unary negate operator. References: - // - https://en.wikipedia.org/wiki/Shunting-yard_algorithm#The_algorithm_in_detail - // - https://wcipeg.com/wiki/Shunting_yard_algorithm#Unary_operators - val operatorStack = Stack() - val outputQueue = ArrayDeque() - var lastToken: MathTokenizer.Token? = null - for (token in tokenize(literalExpression, allowedVariables)) { - when (val parsedToken = ParsedToken.parseToken(token, lastToken)) { - is ParsedToken.Groupable.Computable.Operand -> outputQueue += parsedToken - is ParsedToken.Groupable.Computable.Operator -> { - val parsedOperator = parsedToken.parsedOperator - val precedence = parsedOperator.precedence - val isUnaryOperator = parsedOperator is ParsedOperator.UnaryOperator - while (operatorStack.isNotEmpty()) { - val top = operatorStack.peek() - if (top !is ParsedToken.Groupable.Computable.Operator) break - val topPrecedence = top.parsedOperator.precedence - if (topPrecedence < precedence) break - if (isUnaryOperator) break // Unary operators do not pop operators. - if (topPrecedence == precedence && - parsedOperator is ParsedOperator.BinaryOperator && - parsedOperator.associativity != ParsedOperator.Associativity.LEFT - ) break - operatorStack.pop() - outputQueue += top - } - operatorStack.push(parsedToken) - } - is ParsedToken.Groupable.OpenParenthesis -> { - operatorStack.push(parsedToken) - } - is ParsedToken.CloseParenthesis -> { - while (operatorStack.isNotEmpty()) { - val top = operatorStack.peek() - // The only non-computable, groupable token is OpenParenthesis. - if (top !is ParsedToken.Groupable.Computable) break - operatorStack.pop() - outputQueue += top - } - if (operatorStack.isEmpty() || - operatorStack.peek() !is ParsedToken.Groupable.OpenParenthesis - ) { - return ParseResult.Failure( - "Encountered unexpected close parenthesis at index ${token.column} in " + - token.source - ) - } - // Discard the open parenthesis since it's be finished. - operatorStack.pop() - } - is ParsedToken.FailedToken -> return ParseResult.Failure(parsedToken.getFailureReason()) - } - lastToken = token + private fun parseGenericExpressionGrammar(): MathParsingResult { + // generic_expression_grammar = generic_expression ; + return parseGenericExpression().maybeFail { expression -> checkForLearnerErrors(expression) } + } + + private fun parseGenericEquation(): MathParsingResult { + // algebraic_equation = generic_expression , equals_operator , generic_expression ; + + if (parseContext.hasNextTokenOfType()) { + // If equals starts the string, then there's no LHS. + return EquationMissingLhsOrRhsError.toFailure() + } + + val lhsResult = parseGenericExpression().also { + parseContext.consumeTokenOfType() + }.maybeFail { + if (!parseContext.hasMoreTokens()) { + // If there are no tokens following the equals symbol, then there's no RHS. + EquationMissingLhsOrRhsError + } else null + } + + val rhsResult = lhsResult.flatMap { parseGenericExpression() } + return lhsResult.combineWith(rhsResult) { lhs, rhs -> + MathEquation.newBuilder().apply { + leftSide = lhs + rightSide = rhs + }.build() + } + } + + private fun parseGenericExpression(): MathParsingResult { + // generic_expression = generic_add_sub_expression ; + return parseGenericAddSubExpression() + } + + private fun parseGenericAddSubExpression(): MathParsingResult { + // generic_add_sub_expression = + // generic_mult_div_expression , { generic_add_sub_expression_rhs } ; + return parseGenericBinaryExpression( + parseLhs = this::parseGenericMultDivExpression + ) { nextToken -> + // generic_add_sub_expression_rhs = + // generic_add_expression_rhs | generic_sub_expression_rhs ; + when (nextToken) { + is PlusSymbol -> BinaryOperationRhs( + operator = ADD, + rhsResult = parseGenericAddExpressionRhs() + ) + is MinusSymbol -> BinaryOperationRhs( + operator = SUBTRACT, + rhsResult = parseGenericSubExpressionRhs() + ) + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is FunctionName, is InvalidToken, is LeftParenthesisSymbol, + is MultiplySymbol, is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, + is IncompleteFunctionName, null -> null } + } + } - while (operatorStack.isNotEmpty()) { - when (val top = operatorStack.peek()) { - // The only non-computable, groupable token is OpenParenthesis. - !is ParsedToken.Groupable.Computable -> { - val openParenthesis = top as ParsedToken.Groupable.OpenParenthesis - return ParseResult.Failure( - "Encountered unexpected open parenthesis at index ${openParenthesis.token.column}" + private fun parseGenericAddExpressionRhs(): MathParsingResult { + // generic_add_expression_rhs = plus_operator , generic_mult_div_expression ; + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(ADD) + } else null + }.flatMap { + parseGenericMultDivExpression() + } + } + + private fun parseGenericSubExpressionRhs(): MathParsingResult { + // generic_sub_expression_rhs = minus_operator , generic_mult_div_expression ; + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(SUBTRACT) + } else null + }.flatMap { + parseGenericMultDivExpression() + } + } + + private fun parseGenericMultDivExpression(): MathParsingResult { + // generic_mult_div_expression = + // generic_exp_expression , { generic_mult_div_expression_rhs } ; + return parseGenericBinaryExpression( + parseLhs = this::parseGenericExpExpression + ) { nextToken -> + // generic_mult_div_expression_rhs = + // generic_mult_expression_rhs + // | generic_div_expression_rhs + // | generic_implicit_mult_expression_rhs ; + when (nextToken) { + is MultiplySymbol -> BinaryOperationRhs( + operator = MULTIPLY, + rhsResult = parseGenericMultExpressionRhs() + ) + is DivideSymbol -> BinaryOperationRhs( + operator = DIVIDE, + rhsResult = parseGenericDivExpressionRhs() + ) + is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> BinaryOperationRhs( + operator = MULTIPLY, + rhsResult = parseGenericImplicitMultExpressionRhs(), + isImplicit = true + ) + is VariableName -> { + if (parseContext is AlgebraicExpressionContext) { + BinaryOperationRhs( + operator = MULTIPLY, + rhsResult = parseGenericImplicitMultExpressionRhs(), + isImplicit = true ) + } else null + } + // Not a match to the expression. + is PositiveInteger, is PositiveRealNumber, is EqualsSymbol, is ExponentiationSymbol, + is InvalidToken, is MinusSymbol, is PlusSymbol, is RightParenthesisSymbol, + is IncompleteFunctionName, null -> null + } + } + } + + private fun parseGenericMultExpressionRhs(): MathParsingResult { + // generic_mult_expression_rhs = multiplication_operator , generic_exp_expression ; + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(MULTIPLY) + } else null + }.flatMap { + parseGenericExpExpression() + } + } + + private fun parseGenericDivExpressionRhs(): MathParsingResult { + // generic_div_expression_rhs = division_operator , generic_exp_expression ; + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(DIVIDE) + } else null + }.flatMap { + parseGenericExpExpression() + } + } + + private fun parseGenericImplicitMultExpressionRhs(): MathParsingResult { + // generic_implicit_mult_expression_rhs is either numeric_implicit_mult_expression_rhs or + // algebraic_implicit_mult_or_exp_expression_rhs depending on the current parser context. + return when (parseContext) { + is NumericExpressionContext -> parseNumericImplicitMultExpressionRhs() + is AlgebraicExpressionContext -> parseAlgebraicImplicitMultOrExpExpressionRhs() + } + } + + private fun parseNumericImplicitMultExpressionRhs(): MathParsingResult { + // numeric_implicit_mult_expression_rhs = generic_term_without_unary_without_number ; + return parseGenericTermWithoutUnaryWithoutNumber() + } + + private fun parseAlgebraicImplicitMultOrExpExpressionRhs(): MathParsingResult { + // algebraic_implicit_mult_or_exp_expression_rhs = + // generic_term_without_unary_without_number , [ generic_exp_expression_tail ] ; + val possibleLhs = parseGenericTermWithoutUnaryWithoutNumber() + return if (parseContext.hasNextTokenOfType()) { + parseGenericExpExpressionTail(possibleLhs) + } else possibleLhs + } + + private fun parseGenericExpExpression(): MathParsingResult { + // generic_exp_expression = generic_term_with_unary , [ generic_exp_expression_tail ] ; + val possibleLhs = parseGenericTermWithUnary() + return if (parseContext.hasNextTokenOfType()) { + parseGenericExpExpressionTail(possibleLhs) + } else possibleLhs + } + + // Use tail recursion so that the last exponentiation is evaluated first, and right-to-left + // associativity can be kept via backtracking. + private fun parseGenericExpExpressionTail( + lhsResult: MathParsingResult + ): MathParsingResult { + // generic_exp_expression_tail = exponentiation_operator , generic_exp_expression ; + return BinaryOperationRhs( + operator = EXPONENTIATE, + rhsResult = lhsResult.flatMap { + parseContext.consumeTokenOfType() + }.maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(EXPONENTIATE) + } else null + }.flatMap { + parseGenericExpExpression() + } + ).computeBinaryOperationExpression(lhsResult) + } + + private fun parseGenericTermWithUnary(): MathParsingResult { + // generic_term_with_unary = + // number | generic_term_without_unary_without_number | generic_plus_minus_unary_term ; + return when (val nextToken = parseContext.peekToken()) { + is MinusSymbol, is PlusSymbol -> parseGenericPlusMinusUnaryTerm() + is PositiveInteger, is PositiveRealNumber -> parseNumber().takeUnless { + parseContext.hasNextTokenOfType() + || parseContext.hasNextTokenOfType() + } ?: SpacesBetweenNumbersError.toFailure() + is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> + parseGenericTermWithoutUnaryWithoutNumber() + is VariableName -> { + if (parseContext is AlgebraicExpressionContext) { + parseGenericTermWithoutUnaryWithoutNumber() + } else VariableInNumericExpressionError.toFailure() + } + is DivideSymbol, is ExponentiationSymbol, is MultiplySymbol -> { + val previousToken = parseContext.getPreviousToken() + when { + previousToken is BinaryOperatorToken -> { + SubsequentBinaryOperatorsError( + operator1 = parseContext.extractSubexpression(previousToken), + operator2 = parseContext.extractSubexpression(nextToken) + ).toFailure() } - else -> { - operatorStack.pop() - outputQueue += top + nextToken is BinaryOperatorToken -> { + NoVariableOrNumberBeforeBinaryOperatorError( + operator = nextToken.getBinaryOperator() + ).toFailure() } + else -> GenericError.toFailure() } } + is EqualsSymbol -> { + if (parseContext is AlgebraicExpressionContext && parseContext.isPartOfEquation) { + EquationHasWrongNumberOfEqualsError.toFailure() + } else GenericError.toFailure() + } + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + is RightParenthesisSymbol, null -> GenericError.toFailure() + } + } - // We could alternatively reverse the token stream above & parse prefix notation immediately - // to avoid a second pass over the tokens (since then the expressions could be created - // in-line). However, two passes is simpler (and by using postfix notation we can avoid - // processing tokens that aren't needed if an error occurs during parsing). - val operandStack = Stack() - for (parsedToken in outputQueue) { - when (parsedToken) { - is ParsedToken.Groupable.Computable.Operand -> - operandStack.push(parsedToken.toMathExpression()) - is ParsedToken.Groupable.Computable.Operator -> when (parsedToken.parsedOperator) { - is ParsedOperator.UnaryOperator -> { - if (operandStack.isEmpty()) { - return ParseResult.Failure("Encountered unary operator without operand") - } - operandStack.push(parsedToken.parsedOperator.toMathExpression(operandStack.pop())) - } - is ParsedOperator.BinaryOperator -> { - if (operandStack.size < 2) { - return ParseResult.Failure("Encountered binary operator with missing operand(s)") - } - val rightOperand = operandStack.pop() - val leftOperand = operandStack.pop() - operandStack.push( - parsedToken.parsedOperator.toMathExpression(leftOperand, rightOperand) - ) - } - } + private fun parseGenericTermWithoutUnaryWithoutNumber(): MathParsingResult { + // generic_term_without_unary_without_number is either numeric_term_without_unary_without_number + // or algebraic_term_without_unary_without_number based the current parser context. + return when (parseContext) { + is NumericExpressionContext -> parseNumericTermWithoutUnaryWithoutNumber() + is AlgebraicExpressionContext -> parseAlgebraicTermWithoutUnaryWithoutNumber() + } + } + + private fun parseNumericTermWithoutUnaryWithoutNumber(): MathParsingResult { + // numeric_term_without_unary_without_number = + // generic_function_expression | generic_group_expression | generic_rooted_term ; + return when (val nextToken = parseContext.peekToken()) { + is FunctionName -> parseGenericFunctionExpression() + is LeftParenthesisSymbol -> parseGenericGroupExpression() + is SquareRootSymbol -> parseGenericRootedTerm() + is VariableName -> VariableInNumericExpressionError.toFailure() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, + is RightParenthesisSymbol, null -> GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + } + } + + private fun parseAlgebraicTermWithoutUnaryWithoutNumber(): MathParsingResult { + // algebraic_term_without_unary_without_number = + // generic_function_expression | generic_group_expression | generic_rooted_term | variable ; + return when (val nextToken = parseContext.peekToken()) { + is FunctionName -> parseGenericFunctionExpression() + is LeftParenthesisSymbol -> parseGenericGroupExpression() + is SquareRootSymbol -> parseGenericRootedTerm() + is VariableName -> parseVariable() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, + is RightParenthesisSymbol, null -> GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + } + } + + private fun parseGenericFunctionExpression(): MathParsingResult { + // generic_function_expression = function_name , left_paren , generic_expression , right_paren ; + val funcNameResult = + parseContext.consumeTokenOfType().maybeFail { functionName -> + when { + !functionName.isAllowedFunction -> InvalidFunctionInUseError(functionName.parsedName) + functionName.parsedName == "sqrt" -> null + else -> GenericError + } + }.also { + parseContext.consumeTokenOfType() + } + val argResult = funcNameResult.flatMap { parseGenericExpression() } + val rightParenResult = + argResult.flatMap { + parseContext.consumeTokenOfType { UnbalancedParenthesesError } + } + return funcNameResult.combineWith(argResult, rightParenResult) { funcName, arg, rightParen -> + MathExpression.newBuilder().apply { + parseStartIndex = funcName.startIndex + parseEndIndex = rightParen.endIndex + functionCall = MathFunctionCall.newBuilder().apply { + functionType = MathFunctionCall.FunctionType.SQUARE_ROOT + argument = arg + }.build() + }.build() + } + } + + private fun parseGenericGroupExpression(): MathParsingResult { + // generic_group_expression = left_paren , generic_expression , right_paren ; + val leftParenResult = parseContext.consumeTokenOfType() + val expResult = + leftParenResult.flatMap { + if (parseContext.hasMoreTokens()) { + parseGenericExpression() + } else UnbalancedParenthesesError.toFailure() + } + val rightParenResult = + expResult.flatMap { + parseContext.consumeTokenOfType { UnbalancedParenthesesError } + } + return leftParenResult.combineWith(expResult, rightParenResult) { leftParen, exp, rightParen -> + MathExpression.newBuilder().apply { + parseStartIndex = leftParen.startIndex + parseEndIndex = rightParen.endIndex + group = exp + }.build() + } + } + + private fun parseGenericPlusMinusUnaryTerm(): MathParsingResult { + // generic_plus_minus_unary_term = generic_negated_term | generic_positive_term ; + return when (val nextToken = parseContext.peekToken()) { + is MinusSymbol -> parseGenericNegatedTerm() + is PlusSymbol -> parseGenericPositiveTerm() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is FunctionName, is LeftParenthesisSymbol, is MultiplySymbol, + is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, null -> + GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + } + } + + private fun parseGenericNegatedTerm(): MathParsingResult { + // generic_negated_term = minus_operator , generic_mult_div_expression ; + val minusResult = parseContext.consumeTokenOfType() + val expResult = minusResult.flatMap { parseGenericMultDivExpression() } + return minusResult.combineWith(expResult) { minus, op -> + MathExpression.newBuilder().apply { + parseStartIndex = minus.startIndex + parseEndIndex = op.parseEndIndex + unaryOperation = MathUnaryOperation.newBuilder().apply { + operator = NEGATE + operand = op + }.build() + }.build() + } + } + + private fun parseGenericPositiveTerm(): MathParsingResult { + // generic_positive_term = plus_operator , generic_mult_div_expression ; + val plusResult = parseContext.consumeTokenOfType() + val expResult = plusResult.flatMap { parseGenericMultDivExpression() } + return plusResult.combineWith(expResult) { plus, op -> + MathExpression.newBuilder().apply { + parseStartIndex = plus.startIndex + parseEndIndex = op.parseEndIndex + unaryOperation = MathUnaryOperation.newBuilder().apply { + operator = POSITIVE + operand = op + }.build() + }.build() + } + } + + private fun parseGenericRootedTerm(): MathParsingResult { + // generic_rooted_term = square_root_operator , generic_term_with_unary ; + val sqrtResult = + parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) HangingSquareRootError else null + } + val expResult = sqrtResult.flatMap { parseGenericTermWithUnary() } + return sqrtResult.combineWith(expResult) { sqrtSymbol, op -> + MathExpression.newBuilder().apply { + parseStartIndex = sqrtSymbol.startIndex + parseEndIndex = op.parseEndIndex + functionCall = MathFunctionCall.newBuilder().apply { + functionType = MathFunctionCall.FunctionType.SQUARE_ROOT + argument = op + }.build() + }.build() + } + } + + private fun parseNumber(): MathParsingResult { + // number = positive_real_number | positive_integer ; + return when (val nextToken = parseContext.peekToken()) { + is PositiveInteger -> { + parseContext.consumeTokenOfType().map { positiveInteger -> + MathExpression.newBuilder().apply { + parseStartIndex = positiveInteger.startIndex + parseEndIndex = positiveInteger.endIndex + constant = positiveInteger.toReal() + }.build() + } + } + is PositiveRealNumber -> { + parseContext.consumeTokenOfType().map { positiveRealNumber -> + MathExpression.newBuilder().apply { + parseStartIndex = positiveRealNumber.startIndex + parseEndIndex = positiveRealNumber.endIndex + constant = positiveRealNumber.toReal() + }.build() } } + is DivideSymbol, is EqualsSymbol, is ExponentiationSymbol, is FunctionName, + is LeftParenthesisSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, + is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, null -> + GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + } + } + + private fun parseVariable(): MathParsingResult { + val variableNameResult = + parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.allowsVariables()) GenericError else null + }.maybeFail { variableName -> + return@maybeFail if (parseContext.hasMoreTokens()) { + when (val nextToken = parseContext.peekToken()) { + is PositiveInteger -> + NumberAfterVariableError(nextToken.toReal(), variableName.parsedName) + is PositiveRealNumber -> + NumberAfterVariableError(nextToken.toReal(), variableName.parsedName) + else -> null + } + } else null + } + return variableNameResult.map { variableName -> + MathExpression.newBuilder().apply { + parseStartIndex = variableName.startIndex + parseEndIndex = variableName.endIndex + variable = variableName.parsedName + }.build() + } + } + + private fun parseGenericBinaryExpression( + parseLhs: () -> MathParsingResult, parseRhs: (Token?) -> BinaryOperationRhs? + ): MathParsingResult { + var lastLhsResult = parseLhs() + while (!lastLhsResult.isFailure()) { + // Compute the next LHS if there are further RHS expressions. + lastLhsResult = + parseRhs(parseContext.peekToken()) + ?.computeBinaryOperationExpression(lastLhsResult) + ?: break // Not a match to the expression. + } + return lastLhsResult + } + + private fun checkForLearnerErrors(expression: MathExpression): MathParsingError? { + val firstMultiRedundantGroup = expression.findFirstMultiRedundantGroup() + val nextRedundantGroup = expression.findNextRedundantGroup() + val nextUnaryOperation = expression.findNextRedundantUnaryOperation() + val nextExpWithVariableExp = expression.findNextExponentiationWithVariablePower() + val nextExpWithTooLargePower = expression.findNextExponentiationWithTooLargePower() + val nextExpWithNestedExp = expression.findNextNestedExponentiation() + val nextDivByZero = expression.findNextDivisionByZero() + val disallowedVariables = expression.findAllDisallowedVariables(parseContext) + // Note that the order of checks here is important since errors have precedence, and some are + // redundant and, in the wrong order, may cause the wrong error to be returned. + val includeOptionalErrors = parseContext.errorCheckingMode.includesOptionalErrors() + return when { + includeOptionalErrors && firstMultiRedundantGroup != null -> { + val subExpression = parseContext.extractSubexpression(firstMultiRedundantGroup) + MultipleRedundantParenthesesError(subExpression, firstMultiRedundantGroup) + } + includeOptionalErrors && expression.expressionTypeCase == GROUP -> + SingleRedundantParenthesesError(parseContext.rawExpression, expression) + includeOptionalErrors && nextRedundantGroup != null -> { + val subExpression = parseContext.extractSubexpression(nextRedundantGroup) + RedundantParenthesesForIndividualTermsError(subExpression, nextRedundantGroup) + } + includeOptionalErrors && nextUnaryOperation != null -> SubsequentUnaryOperatorsError + includeOptionalErrors && nextExpWithVariableExp != null -> ExponentIsVariableExpressionError + includeOptionalErrors && nextExpWithTooLargePower != null -> ExponentTooLargeError + includeOptionalErrors && nextExpWithNestedExp != null -> NestedExponentsError + includeOptionalErrors && nextDivByZero != null -> TermDividedByZeroError + includeOptionalErrors && disallowedVariables.isNotEmpty() -> + DisabledVariablesInUseError(disallowedVariables.toList()) + else -> ensureNoRemainingTokens() + } + } - if (operandStack.size != 1) { - return ParseResult.Failure("Failed to resolve expression tree: $operandStack") + private fun ensureNoRemainingTokens(): MathParsingError? { + // Make sure all tokens were consumed (otherwise there are trailing tokens which invalidate the + // whole grammar). + return if (parseContext.hasMoreTokens()) { + when (val nextToken = parseContext.peekToken()) { + is LeftParenthesisSymbol, is RightParenthesisSymbol -> UnbalancedParenthesesError + is EqualsSymbol -> { + if (parseContext is AlgebraicExpressionContext && parseContext.isPartOfEquation) { + EquationHasWrongNumberOfEqualsError + } else GenericError + } + is IncompleteFunctionName -> nextToken.toError() + is InvalidToken -> nextToken.toError() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is ExponentiationSymbol, + is FunctionName, is MinusSymbol, is MultiplySymbol, is PlusSymbol, is SquareRootSymbol, + is VariableName, null -> GenericError } - return ParseResult.Success(operandStack.firstElement()) + } else null + } + + private fun PositiveInteger.toReal(): Real = Real.newBuilder().apply { + integer = parsedValue + }.build() + + private fun PositiveRealNumber.toReal(): Real = Real.newBuilder().apply { + irrational = parsedValue + }.build() + + @Suppress("unused") // The receiver is behaving as a namespace. + private fun IncompleteFunctionName.toError(): MathParsingError = FunctionNameIncompleteError + + private fun InvalidToken.toError(): MathParsingError = + UnnecessarySymbolsError(parseContext.extractSubexpression(this)) + + private fun IncompleteFunctionName.toFailure(): MathParsingResult = toError().toFailure() + + private fun InvalidToken.toFailure(): MathParsingResult = toError().toFailure() + + private sealed class ParseContext(val rawExpression: String) { + val tokens: PeekableIterator by lazy { + PeekableIterator.fromSequence(MathTokenizer.tokenize(rawExpression)) } + private var previousToken: Token? = null + + abstract val errorCheckingMode: ErrorCheckingMode + + abstract fun allowsVariables(): Boolean + + fun hasMoreTokens(): Boolean = tokens.hasNext() + + fun peekToken(): Token? = tokens.peek() /** - * Returns an iterable of tokens by tokenizing the provided expression & accounting for the list - * of allowed variables. This uses [MathTokenizer] & augments it by providing selective support - * for implied multiplication scenarios. + * Returns the last token consumed by [consumeTokenOfType], or null if none. Note: this should + * only be used for error reporting purposes, not for parsing. Using this for parsing would, in + * certain cases, allow for a non-LL(1) grammar which is against one design goal for this + * parser. */ - private fun tokenize( - literalExpression: String, - allowedVariables: List - ): Iterable { - return MathTokenizer.tokenize( - rawLiteral = literalExpression, allowedIdentifiers = allowedVariables - ).adaptTokenStreamForImpliedMultiplication() + fun getPreviousToken(): Token? = previousToken + + inline fun hasNextTokenOfType(): Boolean = peekToken() is T + + inline fun consumeTokenOfType( + missingError: () -> MathParsingError = { GenericError } + ): MathParsingResult { + val maybeToken = tokens.expectNextMatches { it is T } as? T + return maybeToken?.let { token -> + previousToken = token + MathParsingResult.Success(token) + } ?: missingError().toFailure() + } + + fun extractSubexpression(token: Token): String { + return rawExpression.substring(token.startIndex, token.endIndex) } + fun extractSubexpression(expression: MathExpression): String { + return rawExpression.substring(expression.parseStartIndex, expression.parseEndIndex) + } + + class NumericExpressionContext( + rawExpression: String, override val errorCheckingMode: ErrorCheckingMode + ) : ParseContext(rawExpression) { + // Numeric expressions never allow variables. + override fun allowsVariables(): Boolean = false + } + + class AlgebraicExpressionContext( + rawExpression: String, + val isPartOfEquation: Boolean, + private val allowedVariables: List, + override val errorCheckingMode: ErrorCheckingMode + ) : ParseContext(rawExpression) { + fun allowsVariable(variableName: String): Boolean = variableName in allowedVariables + + override fun allowsVariables(): Boolean = true + } + } + + companion object { + enum class ErrorCheckingMode { + REQUIRED_ONLY, + ALL_ERRORS + } + + sealed class MathParsingResult { + data class Success(val result: T) : MathParsingResult() + + data class Failure(val error: MathParsingError) : MathParsingResult() + } + + fun parseNumericExpression( + rawExpression: String, errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS + ): MathParsingResult = + createNumericParser(rawExpression, errorCheckingMode).parseGenericExpressionGrammar() + + fun parseAlgebraicExpression( + rawExpression: String, + allowedVariables: List, + errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS + ): MathParsingResult { + return createAlgebraicParser( + rawExpression, isPartOfEquation = false, allowedVariables, errorCheckingMode + ).parseGenericExpressionGrammar() + } + + fun parseAlgebraicEquation( + rawExpression: String, + allowedVariables: List, + errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS + ): MathParsingResult { + return createAlgebraicParser( + rawExpression, isPartOfEquation = true, allowedVariables, errorCheckingMode + ).parseGenericEquationGrammar() + } + + private fun createNumericParser( + rawExpression: String, errorCheckingMode: ErrorCheckingMode + ): MathExpressionParser = + MathExpressionParser(NumericExpressionContext(rawExpression, errorCheckingMode)) + + private fun createAlgebraicParser( + rawExpression: String, + isPartOfEquation: Boolean, + allowedVariables: List, + errorCheckingMode: ErrorCheckingMode + ): MathExpressionParser { + return MathExpressionParser( + AlgebraicExpressionContext( + rawExpression, isPartOfEquation, allowedVariables, errorCheckingMode + ) + ) + } + + private fun ErrorCheckingMode.includesOptionalErrors() = this == ErrorCheckingMode.ALL_ERRORS + + private fun MathParsingError.toFailure(): MathParsingResult = + MathParsingResult.Failure(this) + + private fun MathParsingResult.isFailure() = this is MathParsingResult.Failure + /** - * Returns a new [Iterable] wrapped around the specified one with additional support for - * injecting synthesized tokens, as needed, into the token stream in order to support implied - * multiplication scenarios. See [ImpliedMultiplicationIteratorAdapter] for specifics on how - * this is implemented & the cases supported. + * Maps [this] result to a new value. Note that this lazily uses the provided function (i.e. + * it's only used if [this] result is passing, otherwise the method will short-circuit a failure + * state so that [this] result's failure is preserved). + * + * @param operation computes a new success result given the current successful result value + * @return a new [MathParsingResult] with a successful result provided by the operation, or the + * preserved failure of [this] result */ - private fun Iterable.adaptTokenStreamForImpliedMultiplication(): - Iterable { - val baseIterable = this - return object : Iterable { - override fun iterator(): Iterator = - ImpliedMultiplicationIteratorAdapter(baseIterable.iterator()) - } + private fun MathParsingResult.map( + operation: (T1) -> T2 + ): MathParsingResult = flatMap { result -> MathParsingResult.Success(operation(result)) } + + /** + * Maps [this] result to a new value. Note that this lazily uses the provided function (i.e. + * it's only used if [this] result is passing, otherwise the method will short-circuit a failure + * state so that [this] result's failure is preserved). + * + * @param operation computes a new result (either a success or failure) given the current + * successful result value + * @return a new [MathParsingResult] with either a result provided by the operation, or the + * preserved failure of [this] result + */ + private fun MathParsingResult.flatMap( + operation: (T1) -> MathParsingResult + ): MathParsingResult { + return when (this) { + is MathParsingResult.Success -> operation(result) + is MathParsingResult.Failure -> error.toFailure() } + } /** - * Returns whether this token, as a previous token (potentially null for the first token of the - * stream) indicates that the token immediately following it could be a unary operator (if other - * sufficient conditions are met, such as the operator matches an expected unary operator - * symbol). + * Potentially changes [this] result into a failure based on the provided [operation]. Note that + * this function lazily uses the operation (i.e. it's only called if [this] result is in a + * passing state), and the returned result will only be in a failing state if [operation] + * returns a non-null error. + * + * @param operation computes a failure error, or null if no error was determined, given the + * current successful result value + * @return either [this] or a failing result if [operation] was called & returned a non-null + * error */ - private fun MathTokenizer.Token?.doesSuggestNegationInNextToken(): Boolean { - // A minus operator at the beginning of the stream, after a group is opened, and after - // another operator is always a unary negate operator. - return this == null || this is OpenParenthesis || this is Operator + private fun MathParsingResult.maybeFail( + operation: (T) -> MathParsingError? + ): MathParsingResult = flatMap { result -> operation(result)?.toFailure() ?: this } + + /** + * Calls an operation if [this] operation isn't already failing, and returns a failure only if + * that operation's result is a failure (otherwise returns [this] result). This function can be + * useful to ensure that subsequent operations are successful even when those operations' + * results are never directly used. + * + * @param operation computes a new result that, when failing, will result in a failing result + * returned from this function. This is only called if [this] result is currently + * successful. + * @return either [this] (iff either this result is failing, or the result of [operation] is a + * success), or the failure returned by [operation] + */ + private fun MathParsingResult.also( + operation: () -> MathParsingResult + ): MathParsingResult = flatMap { + when (val other = operation()) { + is MathParsingResult.Success -> this + is MathParsingResult.Failure -> other.error.toFailure() + } } /** - * Corresponds to an interpreted parsing of a mathematical token that can be analyzed and, in - * some cases, converted to a [MathExpression]. + * Combines [this] result with another result, given a specific combination function. * - * Note that this class & its subclasses heavily rely on various levels of sealed class - * inheritance to tighten the contracts around all types of tokens to facilitate writing highly - * robust code. See the separate classes to get an idea on how the structure is laid out. + * @param other the result to combine with [this] result + * @param combine computes a new value given the result from [this] and [other]. Note that this + * is only called if both results are successful, and the corresponding successful values + * are provided in-order ([this] result's value is the first parameter, and [other]'s is the + * second). + * @return either [this] result's or [other]'s failure, if either are failing, or a successful + * result containing the value computed by [combine] */ - private sealed class ParsedToken { - companion object { - /** - * Returns a new [ParsedToken] for the specified token (& potentially considering the - * previous token), or null if the token corresponds to an operator that isn't recognized. - */ - fun parseToken(token: MathTokenizer.Token, lastToken: MathTokenizer.Token?): ParsedToken? { - return when (token) { - is WholeNumber -> Groupable.Computable.Operand.WholeNumber(token.value) - is DecimalNumber -> Groupable.Computable.Operand.DecimalNumber(token.value) - is Identifier -> Groupable.Computable.Operand.Identifier(token.name) - is Operator -> - Groupable.Computable.Operator( - ParsedOperator.parseOperator(token, lastToken) - ?: return FailedToken.InvalidOperator(token.operator) - ) - is OpenParenthesis -> Groupable.OpenParenthesis(token) - is MathTokenizer.Token.CloseParenthesis -> CloseParenthesis - is InvalidIdentifier -> FailedToken.InvalidIdentifier(token.name) - is InvalidToken -> FailedToken.InvalidToken(token) - } + private fun MathParsingResult.combineWith( + other: MathParsingResult, + combine: (I1, I2) -> O, + ): MathParsingResult { + return flatMap { result -> + other.map { otherResult -> + combine(result, otherResult) } } + } - /** - * Corresponds to a set of tokens that represent groupable components (e.g. open parenthesis, - * constants, etc.). Closed parenthesis is not included since that ends a group rather than - * begins/participates in one. - * - * This class exists to simplify error-handling in the shunting-yard algorithm. - */ - sealed class Groupable : ParsedToken() { - /** - * Corresponds to tokens that are computable (that is, can be converted to - * [MathExpression]s). - */ - sealed class Computable : Groupable() { - /** Corresponds to an operand for a unary or binary operation. */ - sealed class Operand : Computable() { - /** Returns a [MathExpression] representation of this operand. */ - abstract fun toMathExpression(): MathExpression - - /** An operand that is a whole number (e.g. '2'). */ - data class WholeNumber(private val value: Int) : Operand() { - override fun toMathExpression(): MathExpression = - MathExpression.newBuilder() - .setConstant( - Real.newBuilder().setRational( - Fraction.newBuilder().setWholeNumber(value).setDenominator(1) - ) - ).build() - } - - /** An operand that's a decimal (e.g. '3.14'). */ - data class DecimalNumber(private val value: Double) : Operand() { - override fun toMathExpression(): MathExpression = - MathExpression.newBuilder() - .setConstant(Real.newBuilder().setIrrational(value)) - .build() - } - - /** - * An operand that's an identifier (e.g. 'x') which will likely be treated as a - * variable. - */ - data class Identifier(private val name: String) : Operand() { - override fun toMathExpression(): MathExpression = - MathExpression.newBuilder().setVariable(name).build() - } + /** + * Performs the same operation as the other [combineWith] function, except with three + * [MathParsingResult]s, instead. + */ + private fun MathParsingResult.combineWith( + other1: MathParsingResult, + other2: MathParsingResult, + combine: (I1, I2, I3) -> O, + ): MathParsingResult { + return flatMap { result -> + other1.flatMap { otherResult1 -> + other2.map { otherResult2 -> + combine(result, otherResult1, otherResult2) } - - /** - * Corresponds to an operator (binary or unary). See [ParsedOperator] for supported - * operators. - */ - data class Operator(val parsedOperator: ParsedOperator) : Computable() } - - /** Corresponds to an open parenthesis token which begins a grouped expression. */ - data class OpenParenthesis(val token: MathTokenizer.Token) : Groupable() } + } - /** Corresponds to a close parenthesis token which ends a grouped expression. */ - object CloseParenthesis : ParsedToken() - - /** Corresponds to a token that represents a failure during tokenization or parsing. */ - sealed class FailedToken : ParsedToken() { - /** - * Returns the reason the failure token was created. This is not meant to be shown to end - * users, only developers. - */ - abstract fun getFailureReason(): String - - /** - * Indicates an invalid operator was encountered. This typically means the tokenizer - * supports operators that the parser does not. - */ - data class InvalidOperator(val operator: Char) : FailedToken() { - override fun getFailureReason(): String = "Encountered unexpected operator: $operator" + private data class BinaryOperationRhs( + val operator: MathBinaryOperation.Operator, + val rhsResult: MathParsingResult, + val isImplicit: Boolean = false + ) { + fun computeBinaryOperationExpression( + lhsResult: MathParsingResult + ): MathParsingResult { + return lhsResult.combineWith(rhsResult) { lhs, rhs -> + MathExpression.newBuilder().apply { + parseStartIndex = lhs.parseStartIndex + parseEndIndex = rhs.parseEndIndex + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = this@BinaryOperationRhs.operator + leftOperand = lhs + rightOperand = rhs + isImplicit = this@BinaryOperationRhs.isImplicit + }.build() + }.build() } + } + } - /** - * Indicates an identifier was encountered that doesn't correspond to any of the allowed - * variables passed to the parser during parsing time. - */ - data class InvalidIdentifier(val name: String) : FailedToken() { - override fun getFailureReason(): String = "Encountered invalid identifier: $name" + private fun MathExpression.findFirstMultiRedundantGroup(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findFirstMultiRedundantGroup() + ?: binaryOperation.rightOperand.findFirstMultiRedundantGroup() } + UNARY_OPERATION -> unaryOperation.operand.findFirstMultiRedundantGroup() + FUNCTION_CALL -> functionCall.argument.findFirstMultiRedundantGroup() + GROUP -> group.takeIf { it.expressionTypeCase == GROUP } + ?: group.findFirstMultiRedundantGroup() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } - /** Indicates an invalid token was encountered during tokenization. */ - data class InvalidToken(private val token: MathTokenizer.Token) : FailedToken() { - override fun getFailureReason(): String = - "Encountered unexpected symbol at index ${token.column} in ${token.source}" + private fun MathExpression.findNextRedundantGroup(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findNextRedundantGroup() + ?: binaryOperation.rightOperand.findNextRedundantGroup() } + UNARY_OPERATION -> unaryOperation.operand.findNextRedundantGroup() + FUNCTION_CALL -> functionCall.argument.findNextRedundantGroup() + GROUP -> group.takeIf { + it.expressionTypeCase in listOf(CONSTANT, VARIABLE) + } ?: group.findNextRedundantGroup() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null } } - /** Corresponds to an operator parsed from an operator token with defined precedence. */ - private sealed class ParsedOperator(val precedence: Int) { - companion object { - /** - * Returns a new [ParsedOperator] given the specified [Operator], or null if the operator is - * not recognized. - * - * This uses the previous token to determine whether this operator is unary or binary. - */ - fun parseOperator(operator: Operator, lastToken: MathTokenizer.Token?): ParsedOperator? { - return when (operator.operator) { - '+' -> Add - '-' -> if (lastToken.doesSuggestNegationInNextToken()) Negate else Subtract - '*' -> Multiply - '/' -> Divide - '^' -> Exponentiate - else -> null - } + private fun MathExpression.findNextRedundantUnaryOperation(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findNextRedundantUnaryOperation() + ?: binaryOperation.rightOperand.findNextRedundantUnaryOperation() } + UNARY_OPERATION -> unaryOperation.operand.takeIf { + it.expressionTypeCase == UNARY_OPERATION + } ?: unaryOperation.operand.findNextRedundantUnaryOperation() + FUNCTION_CALL -> functionCall.argument.findNextRedundantUnaryOperation() + GROUP -> group.findNextRedundantUnaryOperation() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null } + } - /** - * Corresponds to relative associativity for other encountered operations whose operators are - * at the same level of precedence. - */ - enum class Associativity { - LEFT, - RIGHT, - } - - /** Corresponds to a binary operation (e.g. 'x + y'). */ - abstract class BinaryOperator( - precedence: Int, - val associativity: Associativity, - private val protoOperator: MathBinaryOperation.Operator - ) : ParsedOperator(precedence) { - /** Returns a [MathExpression] representation of this parsed operator. */ - fun toMathExpression( - leftOperand: MathExpression, - rightOperand: MathExpression - ): MathExpression = - MathExpression.newBuilder() - .setBinaryOperation( - MathBinaryOperation.newBuilder() - .setOperator(protoOperator) - .setLeftOperand(leftOperand) - .setRightOperand(rightOperand) - ).build() - } - - /** Corresponds to a unary operation (e.g. '-x'). */ - abstract class UnaryOperator( - precedence: Int, - private val protoOperator: MathUnaryOperation.Operator - ) : ParsedOperator(precedence) { - /** Returns a [MathExpression] representation of this parsed operator. */ - fun toMathExpression(operand: MathExpression): MathExpression = - MathExpression.newBuilder() - .setUnaryOperation( - MathUnaryOperation.newBuilder() - .setOperator(protoOperator) - .setOperand(operand) - ).build() - } - - /** Corresponds to the addition operation, e.g.: 1 + 2. */ - object Add : BinaryOperator( - precedence = 1, - associativity = Associativity.LEFT, - protoOperator = MathBinaryOperation.Operator.ADD - ) - - /** Corresponds to the subtraction operation, e.g.: 1 - 2. */ - object Subtract : BinaryOperator( - precedence = Add.precedence, - associativity = Associativity.LEFT, - protoOperator = MathBinaryOperation.Operator.SUBTRACT - ) + private fun MathExpression.findNextExponentiationWithVariablePower(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == EXPONENTIATE + && binaryOperation.rightOperand.isVariableExpression() + } ?: binaryOperation.leftOperand.findNextExponentiationWithVariablePower() + ?: binaryOperation.rightOperand.findNextExponentiationWithVariablePower() + } + UNARY_OPERATION -> unaryOperation.operand.findNextExponentiationWithVariablePower() + FUNCTION_CALL -> functionCall.argument.findNextExponentiationWithVariablePower() + GROUP -> group.findNextExponentiationWithVariablePower() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } - /** Corresponds to the multiplication operation, e.g.: 1 * 2. */ - object Multiply : BinaryOperator( - precedence = Add.precedence + 1, - associativity = Associativity.LEFT, - protoOperator = MathBinaryOperation.Operator.MULTIPLY - ) + private fun MathExpression.findNextExponentiationWithTooLargePower(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == EXPONENTIATE + && binaryOperation.rightOperand.expressionTypeCase == CONSTANT + && binaryOperation.rightOperand.constant.toDouble() > 5.0 + } ?: binaryOperation.leftOperand.findNextExponentiationWithTooLargePower() + ?: binaryOperation.rightOperand.findNextExponentiationWithTooLargePower() + } + UNARY_OPERATION -> unaryOperation.operand.findNextExponentiationWithTooLargePower() + FUNCTION_CALL -> functionCall.argument.findNextExponentiationWithTooLargePower() + GROUP -> group.findNextExponentiationWithTooLargePower() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } - /** Corresponds to the division operation, e.g.: 1 / 2. */ - object Divide : BinaryOperator( - precedence = Multiply.precedence, - associativity = Associativity.LEFT, - protoOperator = MathBinaryOperation.Operator.DIVIDE - ) + private fun MathExpression.findNextNestedExponentiation(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == EXPONENTIATE + && binaryOperation.rightOperand.containsExponentiation() + } ?: binaryOperation.leftOperand.findNextNestedExponentiation() + ?: binaryOperation.rightOperand.findNextNestedExponentiation() + } + UNARY_OPERATION -> unaryOperation.operand.findNextNestedExponentiation() + FUNCTION_CALL -> functionCall.argument.findNextNestedExponentiation() + GROUP -> group.findNextNestedExponentiation() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } - /** Corresponds to unary negation, e.g.: -1. */ - object Negate : UnaryOperator( - precedence = Multiply.precedence + 1, - protoOperator = MathUnaryOperation.Operator.NEGATE - ) + private fun MathExpression.findNextDivisionByZero(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == DIVIDE + && binaryOperation.rightOperand.expressionTypeCase == CONSTANT + && binaryOperation.rightOperand.constant + .toDouble().absoluteValue.approximatelyEquals(0.0) + } ?: binaryOperation.leftOperand.findNextDivisionByZero() + ?: binaryOperation.rightOperand.findNextDivisionByZero() + } + UNARY_OPERATION -> unaryOperation.operand.findNextDivisionByZero() + FUNCTION_CALL -> functionCall.argument.findNextDivisionByZero() + GROUP -> group.findNextDivisionByZero() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } - /** Corresponds to the exponentiation operation, e.g.: 1 ^ 2. */ - object Exponentiate : BinaryOperator( - precedence = Negate.precedence + 1, - associativity = Associativity.RIGHT, - protoOperator = MathBinaryOperation.Operator.EXPONENTIATE - ) + private fun MathExpression.findAllDisallowedVariables(context: ParseContext): Set { + return if (context is AlgebraicExpressionContext) { + findAllDisallowedVariablesAux(context) + } else setOf() } - /** - * An adapter of the iterator returned by [MathTokenizer] that will synthesize operators in - * cases when there's implied multiplication (e.g. 2xy should be interpreted as 2*x*y). - */ - private class ImpliedMultiplicationIteratorAdapter( - private val baseIterator: Iterator - ) : Iterator { - private var lastToken: MathTokenizer.Token? = null - private var nextToken: MathTokenizer.Token? = null - - // The base iterator's hasNext() is sufficient since this adapter will only ever synthesize - // new tokens *before* another token (since the synthesized tokens are always binary - // operators), except in the end-of-stream case (since the adapter might have a next token - // saved). - override fun hasNext(): Boolean = baseIterator.hasNext() || nextToken != null - - override fun next(): MathTokenizer.Token { - val (currentToken, newNextToken) = - computeCurrentTokenState(lastToken, nextToken ?: baseIterator.next()) - nextToken = newNextToken - lastToken = currentToken - return currentToken - } - - /** - * Returns a destructible data object of two elements defining first, the next token to - * return, and second, the token that should be used the next time next() is called, or null - * if none should be used (meaning a new element should be retrieved from the backing iterator - * on the next call to next()). - * - * @param lastToken the previous token provided via next(), or null if none - * @param nextToken the next token that should be provided to the user - */ - private fun computeCurrentTokenState( - lastToken: MathTokenizer.Token?, - nextToken: MathTokenizer.Token - ): NewTokenState { - return when { - lastToken.impliesMultiplicationWith(nextToken) -> NewTokenState( - currentToken = synthesizeMultiplicationOperatorToken(), nextToken = nextToken - ) - else -> NewTokenState(currentToken = nextToken, nextToken = null) + private fun MathExpression.findAllDisallowedVariablesAux( + context: AlgebraicExpressionContext + ): Set { + return when (expressionTypeCase) { + VARIABLE -> if (context.allowsVariable(variable)) setOf() else setOf(variable) + BINARY_OPERATION -> { + binaryOperation.leftOperand.findAllDisallowedVariablesAux(context) + + binaryOperation.rightOperand.findAllDisallowedVariablesAux(context) } + UNARY_OPERATION -> unaryOperation.operand.findAllDisallowedVariablesAux(context) + FUNCTION_CALL -> functionCall.argument.findAllDisallowedVariablesAux(context) + GROUP -> group.findAllDisallowedVariablesAux(context) + CONSTANT, EXPRESSIONTYPE_NOT_SET, null -> setOf() } + } - /** - * Returns a new multiplication operator token to enable the parser to imply multiplication in - * certain contexts. - */ - private fun synthesizeMultiplicationOperatorToken(): MathTokenizer.Token { - return Operator(source = "", column = 0, operator = '*') - } - - /** Returns whether this token is constant (e.g. a whole number or variable). */ - private fun MathTokenizer.Token.isConstant(): Boolean { - return this is WholeNumber || this is DecimalNumber - } - - /** - * Returns whether this token is a variable (which, at the moment, is determined based on - * whether it's an identifier since identifiers aren't currently used for any other purpose). - */ - private fun MathTokenizer.Token.isVariable(): Boolean { - return this is Identifier - } - - /** - * Returns whether this token is a variable or constant (see the corresponding functions above - * for specifics on what each means in practice). - */ - private fun MathTokenizer.Token.isVariableOrConstant(): Boolean { - return this.isVariable() || this.isConstant() - } - - /** - * Returns whether this token, in conjunction with the specified token, indicates a scenario - * where multiplication should be implied. See the implementation for specifics. - */ - private fun MathTokenizer.Token?.impliesMultiplicationWith( - nextToken: MathTokenizer.Token - ): Boolean { - // Two consecutive tokens imply multiplication iff they are both variables, or one is a - // variable and the other is a constant. Or, a variable/constant is followed by an open - // parenthesis or a close parenthesis is followed by a variable/constant. Finally, two - // consecutive sets of parentheses also imply multiplication. - return when { - this == null -> false - this.isVariable() && nextToken.isVariable() -> true - this.isConstant() && nextToken.isVariable() -> true - this.isVariable() && nextToken.isConstant() -> true - this.isVariableOrConstant() && nextToken is OpenParenthesis -> true - this is MathTokenizer.Token.CloseParenthesis && nextToken.isVariableOrConstant() -> true - this is MathTokenizer.Token.CloseParenthesis && nextToken is OpenParenthesis -> true - else -> false + private fun MathExpression.isVariableExpression(): Boolean { + return when (expressionTypeCase) { + VARIABLE -> true + BINARY_OPERATION -> { + binaryOperation.leftOperand.isVariableExpression() + || binaryOperation.rightOperand.isVariableExpression() } + UNARY_OPERATION -> unaryOperation.operand.isVariableExpression() + FUNCTION_CALL -> functionCall.argument.isVariableExpression() + GROUP -> group.isVariableExpression() + CONSTANT, EXPRESSIONTYPE_NOT_SET, null -> false } + } - /** - * Temporary data object to signal to the adapter which token to return to the parser & which, - * if any, to cache for future calls to the iterator. - */ - private data class NewTokenState( - val currentToken: MathTokenizer.Token, - val nextToken: MathTokenizer.Token? - ) + private fun MathExpression.containsExponentiation(): Boolean { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.operator == EXPONENTIATE + || binaryOperation.leftOperand.containsExponentiation() + || binaryOperation.rightOperand.containsExponentiation() + } + UNARY_OPERATION -> unaryOperation.operand.containsExponentiation() + FUNCTION_CALL -> functionCall.argument.containsExponentiation() + GROUP -> group.containsExponentiation() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> false + } } } } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt index 053da9d8ead..47e1b8f2d76 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt @@ -1,503 +1,359 @@ package org.oppia.android.util.math -import java.lang.IllegalStateException -import java.util.ArrayDeque -import java.util.Locale - -private const val DECIMAL_POINT = '.' -private const val LEFT_PARENTHESIS = '(' -private const val RIGHT_PARENTHESIS = ')' -private const val CONVENTIONAL_MULTIPLICATION_SIGN = '*' -private const val CONVENTIONAL_DIVISION_SIGN = '/' -private const val FORMAL_MULTIPLICATION_SIGN = '×' -private const val FORMAL_DIVISION_SIGN = '÷' - -// Only consider standard horizontal/vertical whitespace. -private val VALID_WHITESPACE = listOf(' ', '\t', '\n', '\r') -private val VALID_OPERATORS = listOf( - CONVENTIONAL_MULTIPLICATION_SIGN, '-', '+', CONVENTIONAL_DIVISION_SIGN, '^', - FORMAL_MULTIPLICATION_SIGN, FORMAL_DIVISION_SIGN -) -private val VALID_DIGITS = listOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9') - -/** - * Container class for functionality corresponding to tokenization of mathematics expressions. - * - * The current tokenization only supports basic polynomials. - */ -class MathTokenizer { - /** Corresponds to a token that may be found in a tokenized math expression. */ - sealed class Token { - abstract val source: String - abstract val column: Int - - /** Returns a human-readable string for this token. */ - abstract fun toReadableString(): String - - /** Corresponds to a valid operator: [*+-/^] */ - data class Operator( - override val source: String, - override val column: Int, - val operator: Char - ) : Token() { - override fun toReadableString(): String = operator.toString() - } - - /** Corresponds to a whole, non-decimal number: [0-9]+ */ - data class WholeNumber( - override val source: String, - override val column: Int, - val value: Int - ) : Token() { - override fun toReadableString(): String = value.toString() - } - - /** Corresponds to a decimal number (note that the decimal is required): ([0-9]*.[0-9]+) */ - data class DecimalNumber( - override val source: String, - override val column: Int, - val value: Double - ) : Token() { - override fun toReadableString(): String = value.toString() - } - - /** Corresponds to an identifier, single or multi-letter (typically for variables). */ - data class Identifier( - override val source: String, - override val column: Int, - val name: String - ) : Token() { - override fun toReadableString(): String = name - } - - /** Corresponds to an open parenthesis: ( */ - data class OpenParenthesis(override val source: String, override val column: Int) : Token() { - override fun toReadableString(): String = LEFT_PARENTHESIS.toString() - } - - /** Corresponds to a close parenthesis: ) */ - data class CloseParenthesis(override val source: String, override val column: Int) : Token() { - override fun toReadableString(): String = RIGHT_PARENTHESIS.toString() - } - - /** Corresponds to an invalid identifier that was encountered. */ - data class InvalidIdentifier( - override val source: String, - override val column: Int, - val name: String - ) : Token() { - override fun toReadableString(): String = "Invalid identifier: $name" - } - - /** Corresponds to an invalid token that was encountered. */ - data class InvalidToken( - override val source: String, - override val column: Int, - val token: String - ) : Token() { - override fun toReadableString(): String = "Invalid token: $token" - } - } - +import java.lang.StringBuilder +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE + +// TODO: rename to MathTokenizer & add documentation. +// TODO: consider a more efficient implementation that uses 1 underlying buffer (which could still +// be sequence-backed) with a forced lookahead-of-1 API, to also avoid rebuffering when parsing +// sequences of characters like for integers. + +// TODO: add customization to omit certain symbols (such as variables for numeric expressions?) +class MathTokenizer private constructor() { companion object { - /** - * Returns an iterable that provides a lazy iterator over tokens that will only parse tokens as - * requested. - * - * Note that the returned iterable is thread-safe, but the iterator it provides is not. Callers - * should fully tokenize the stream by copying the iterable to list before performing - * multi-threaded operations on the results, or synchronize access to the iterator. - * - * Note also that tokenization is done in a case-insensitive manner. - * - * Note also that both single-letter and multi-letter identifiers are supported, but their - * behaviors differ. Single-letter identifiers support implicit multiplication (e.g. 'xy' is - * equivalent to 'x*y') but multi-letter identifiers do not (e.g. pilambda) since in the latter - * case 'pilambda' is treated as a single, unknown identifier. Note that in cases of ambiguity, - * multi-letter identifiers take precedence. For example, if the provided identifiers are 'p', - * 'i', and 'pi', then encounters of 'pi' will use the multi-letter identifier rather than p*i. - * - * @param allowedIdentifiers a list of acceptable identifiers that can be parsed (these may be - * more than one letter long). Any identifiers encountered that aren't part of this list - * will result in an invalid identifier token being returned. Note that identifiers must - * only contain strings with letters (per the definition of Character.isLetter()). This list - * can be empty (in which case all encountered identifiers will be presumed invalid). - */ - fun tokenize( - rawLiteral: String, - allowedIdentifiers: List - ): Iterable { - // Verify that the provided identifiers are all valid. - for (identifier in allowedIdentifiers) { - if (identifier.any(Char::isNotLetter)) { - throw IllegalArgumentException("Identifier contains non-letters: $identifier") - } - if (identifier.isEmpty()) { - throw IllegalArgumentException("Encountered empty identifier in allowed identifier list") + fun tokenize(input: String): Sequence = tokenize(input.toCharArray().asSequence()) + + fun tokenize(input: Sequence): Sequence { + val chars = PeekableIterator.fromSequence(input) + return generateSequence { + // Consume any whitespace that might precede a valid token. + chars.consumeWhitespace() + + // Parse the next token from the underlying sequence. + when (chars.peek()) { + in '0'..'9' -> tokenizeIntegerOrRealNumber(chars) + in 'a'..'z', in 'A'..'Z' -> tokenizeVariableOrFunctionName(chars) + '√' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.SquareRootSymbol(startIndex, endIndex) + } + '+' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.PlusSymbol(startIndex, endIndex) + } + // TODO: add tests for different subtraction/minus symbols. + '-', '−' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.MinusSymbol(startIndex, endIndex) + } + '*', '×' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.MultiplySymbol(startIndex, endIndex) + } + '/', '÷' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.DivideSymbol(startIndex, endIndex) + } + '^' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.ExponentiationSymbol(startIndex, endIndex) + } + '=' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.EqualsSymbol(startIndex, endIndex) + } + '(' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.LeftParenthesisSymbol(startIndex, endIndex) + } + ')' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.RightParenthesisSymbol(startIndex, endIndex) + } + null -> null // End of stream. + // Invalid character. + else -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.InvalidToken(startIndex, endIndex) + } } } + } - val lowercaseLiteral = rawLiteral.toLowerCase(Locale.getDefault()) - val lowercaseIdentifiers = allowedIdentifiers.map { - it.toLowerCase(Locale.getDefault()) - }.toSet() - return object : Iterable { - override fun iterator(): Iterator = - Tokenizer(lowercaseLiteral, lowercaseIdentifiers.toList()) + private fun tokenizeIntegerOrRealNumber(chars: PeekableIterator): Token { + val startIndex = chars.getRetrievalCount() + val integerPart1 = + parseInteger(chars) + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) + chars.consumeWhitespace() // Whitespace is allowed between digits and the '.'. + return if (chars.peek() == '.') { + chars.next() // Parse the "." since it will be re-added later. + chars.consumeWhitespace() // Whitespace is allowed between the '.' and following digits. + + // Another integer must follow the ".". + val integerPart2 = parseInteger(chars) + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) + + // TODO: validate that the result isn't NaN or INF. + val doubleValue = "$integerPart1.$integerPart2".toDoubleOrNull() + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) + Token.PositiveRealNumber(doubleValue, startIndex, endIndex = chars.getRetrievalCount()) + } else { + Token.PositiveInteger( + integerPart1.toIntOrNull() + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()), + startIndex, + endIndex = chars.getRetrievalCount() + ) } } - /** - * Tokenizer for math expressions. Standard whitespace is ignored (including newlines). See - * subclasses of the Token class for valid tokens & their corresponding patterns. - * - * Note that this class is only safe to access on a single thread. - */ - private class Tokenizer( - private val source: String, - private val allowedIdentifiers: List - ) : Iterator { - private val singleLetterIdentifiers: List by lazy { - allowedIdentifiers.filter { it.length == 1 }.map(String::first) - } - private val multiLetterIdentifiers: List by lazy { - allowedIdentifiers.filter { it.length > 1 } - } - private val parsedIdentifierCache = ArrayDeque() - private val buffer = source.toCharArray() - private var currentIndex = 0 - private var nextToken: Token? = null + private fun tokenizeVariableOrFunctionName(chars: PeekableIterator): Token { + val startIndex = chars.getRetrievalCount() + val firstChar = chars.next() - override fun hasNext(): Boolean = maybeParseNextToken() + // latin_letter = lowercase_latin_letter | uppercase_latin_letter ; + // variable = latin_letter ; + return tokenizeFunctionName(firstChar, startIndex, chars) + ?: Token.VariableName( + firstChar.toString(), startIndex, endIndex = chars.getRetrievalCount() + ) + } - override fun next(): Token { - if (!hasNext()) { - throw IllegalStateException("Reach end-of-stream") + private fun tokenizeFunctionName( + currChar: Char, startIndex: Int, chars: PeekableIterator + ): Token? { + // allowed_function_name = "sqrt" ; + // disallowed_function_name = + // "exp" | "log" | "log10" | "ln" | "sin" | "cos" | "tan" | "cot" | "csc" + // | "sec" | "atan" | "asin" | "acos" | "abs" ; + // function_name = allowed_function_name | disallowed_function_name ; + val nextChar = chars.peek() + return when (currChar) { + 'a' -> { + // abs, acos, asin, atan, or variable. + when (nextChar) { + 'b' -> + tokenizeExpectedFunction(name = "abs", isAllowedFunction = false, startIndex, chars) + 'c' -> + tokenizeExpectedFunction(name = "acos", isAllowedFunction = false, startIndex, chars) + 's' -> + tokenizeExpectedFunction(name = "asin", isAllowedFunction = false, startIndex, chars) + 't' -> + tokenizeExpectedFunction(name = "atan", isAllowedFunction = false, startIndex, chars) + else -> null // Must be a variable. + } } - val token = checkNotNull(nextToken) { - "Encountered comodification in iterator: iterator modified during tokenization" + 'c' -> { + // cos, cot, csc, or variable. + when (nextChar) { + 'o' -> { + chars.next() // Skip the 'o' to go to the last character. + val name = if (chars.peek() == 's') { + chars.expectNextMatches { it == 's' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + "cos" + } else { + // Otherwise, it must be 'c' for 'cot' since the parser can't backtrack. + chars.expectNextMatches { it == 't' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + "cot" + } + Token.FunctionName( + name, isAllowedFunction = false, startIndex, endIndex = chars.getRetrievalCount() + ) + } + 's' -> + tokenizeExpectedFunction(name = "csc", isAllowedFunction = false, startIndex, chars) + else -> null // Must be a variable. + } } - - // Reset the token so the next one can be parsed. - nextToken = null - - return token - } - - private fun maybeParseNextToken(): Boolean { - if (nextToken != null) { - // There's already a token parsed. - return true + 'e' -> { + // exp or variable. + if (nextChar == 'x') { + tokenizeExpectedFunction(name = "exp", isAllowedFunction = false, startIndex, chars) + } else null // Must be a variable. } - - // Note that there's hidden caching built in for identifiers: identifier parsing can yield - // multiple identifiers and only one token is returned at a time, any previously parsed - // identifiers take top precedent. - if (parsedIdentifierCache.isEmpty()) { - // Skip all whitespace before looking for new tokens. - skipWhitespace() - if (isAtEof()) { - // Reach the end of the stream. - return false + 'l' -> { + // ln, log, log10, or variable. + when (nextChar) { + 'n' -> + tokenizeExpectedFunction(name = "ln", isAllowedFunction = false, startIndex, chars) + 'o' -> { + // Skip the 'o'. Following the 'o' must be a 'g' since the parser can't backtrack. + chars.next() + chars.expectNextMatches { it == 'g' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + val name = if (chars.peek() == '1') { + // '10' must be next for 'log10'. + chars.expectNextMatches { it == '1' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + chars.expectNextMatches { it == '0' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + "log10" + } else "log" + Token.FunctionName( + name, isAllowedFunction = false, startIndex, endIndex = chars.getRetrievalCount() + ) + } + else -> null // Must be a variable. } } - - // Otherwise, there's a token to parse (either there's a pending variable or the - // end-of-stream has not yet been reached). Parse it & continue. - nextToken = parseNextToken() - return true + 's' -> { + // sec, sin, sqrt, or variable. + when (nextChar) { + 'e' -> + tokenizeExpectedFunction(name = "sec", isAllowedFunction = false, startIndex, chars) + 'i' -> + tokenizeExpectedFunction(name = "sin", isAllowedFunction = false, startIndex, chars) + 'q' -> + tokenizeExpectedFunction(name = "sqrt", isAllowedFunction = true, startIndex, chars) + else -> null // Must be a variable. + } + } + 't' -> { + // tan or variable. + if (nextChar == 'a') { + tokenizeExpectedFunction(name = "tan", isAllowedFunction = false, startIndex, chars) + } else null // Must be a variable. + } + else -> null // Must be a variable since no known functions match the first character. } + } - /** Returns whether the tokenizer has reached the end of the stream. */ - private fun isAtEof(): Boolean = isEofIndex(currentIndex) + private fun tokenizeExpectedFunction( + name: String, isAllowedFunction: Boolean, startIndex: Int, chars: PeekableIterator + ): Token { + return chars.expectNextCharsForFunctionName(name.substring(1), startIndex) + ?: Token.FunctionName( + name, isAllowedFunction, startIndex, endIndex = chars.getRetrievalCount() + ) + } - /** Skips whitespace. May be called if already at the end of the stream. */ - private fun skipWhitespace() { - advanceIndexTo(seekUntilCharacterNotFound(VALID_WHITESPACE)) - } + private fun tokenizeSymbol(chars: PeekableIterator, factory: (Int, Int) -> Token): Token { + val startIndex = chars.getRetrievalCount() + chars.next() // Parse the symbol. + val endIndex = chars.getRetrievalCount() + return factory(startIndex, endIndex) + } - /** Returns the next parsed token. Must not be called at the end of the stream. */ - private fun parseNextToken(): Token { - // Parse the next token in an order to avoid potential ambiguities (such as between whole & - // decimal numbers). - return retrieveNextParsedIdentifier() - ?: parseOperator() - ?: parseDecimalNumber() - ?: parseWholeNumber() - ?: parseWholeNumber() - ?: parseIdentifiersAndReturnFirst() - ?: parseParenthesis() - ?: parseInvalidToken() + private fun parseInteger(chars: PeekableIterator): String? { + val integerBuilder = StringBuilder() + while (chars.peek() in '0'..'9') { + integerBuilder.append(chars.next()) } + return if (integerBuilder.isNotEmpty()) { + integerBuilder.toString() + } else null // Failed to parse; no digits. + } - /** - * Returns the next identifier in the local cache of parsed identifiers, or null if there are - * none left/available. - */ - private fun retrieveNextParsedIdentifier(): Token? = parsedIdentifierCache.poll() - - /** - * Returns the next operator token or null if the next token is not an operator. Must not be - * called at the end of the stream. - */ - private fun parseOperator(): Token? { - val parsedIndex = currentIndex - val potentialOperator = peekCharacter() - if (potentialOperator !in VALID_OPERATORS) { - // The next character is not a recognized operator. - return null - } + interface UnaryOperatorToken { + fun getUnaryOperator(): MathUnaryOperation.Operator + } - // When interpreting the operator, translate the unicode symbols to conventional symbols to - // simplify upstream parsing. - val parsedOperator = when (potentialOperator) { - FORMAL_MULTIPLICATION_SIGN -> CONVENTIONAL_MULTIPLICATION_SIGN - FORMAL_DIVISION_SIGN -> CONVENTIONAL_DIVISION_SIGN - else -> potentialOperator - } + interface BinaryOperatorToken { + fun getBinaryOperator(): MathBinaryOperation.Operator + } - skipToken() - return Token.Operator(source, parsedIndex, parsedOperator) - } + sealed class Token { + /** The index in the input stream at which point this token begins. */ + abstract val startIndex: Int - /** - * Returns the next decimal token or null if the next token is not a decimal number. Must not - * be called at the end of the stream. - */ - private fun parseDecimalNumber(): Token? { - val parsedIndex = currentIndex - val decimalIndex = seekUntilCharacterNotFound(VALID_DIGITS) - if (isEofIndex(decimalIndex) || buffer[decimalIndex] != DECIMAL_POINT) { - // There is nothing in the stream looking like: [0-9]*\\. - return null - } + /** The (exclusive) index in the input stream at which point this token ends. */ + abstract val endIndex: Int - val numberEndIndex = seekUntilCharacterNotFound(VALID_DIGITS, startIndex = decimalIndex + 1) - if (numberEndIndex == decimalIndex + 1) { - // There are no digits following the period so this isn't a valid decimal. This may - // indicate an incorrectly formatted decimal, but the '.' will be picked up in a later - // token pass. - return null - } + class PositiveInteger( + val parsedValue: Int, override val startIndex: Int, override val endIndex: Int + ) : Token() - // Either the decimal is something.something, or just .something - val value = source.substring( - startIndex = currentIndex, - endIndex = numberEndIndex - ).toDouble() - advanceIndexTo(numberEndIndex) - return Token.DecimalNumber(source, parsedIndex, value) - } + class PositiveRealNumber( + val parsedValue: Double, override val startIndex: Int, override val endIndex: Int + ) : Token() - /** - * Returns the next whole number token or null if the next token is not a whole number. Must - * not be called at the end of the stream. - */ - private fun parseWholeNumber(): Token? { - val parsedIndex = currentIndex - val numberEndIndex = seekUntilCharacterNotFound(VALID_DIGITS) - if (currentIndex == numberEndIndex) { - // The next character is not a digit, so this can't be a whole number. - return null - } + class VariableName( + val parsedName: String, override val startIndex: Int, override val endIndex: Int + ) : Token() - // Ensure the decimal is parsed in base 10 (in case it starts with 0--that shouldn't be - // interpreted as Octal). - val value = source.substring( - startIndex = currentIndex, - endIndex = numberEndIndex - ).toInt(radix = 10) - advanceIndexTo(numberEndIndex) - return Token.WholeNumber(source, parsedIndex, value) - } + class FunctionName( + val parsedName: String, val isAllowedFunction: Boolean, override val startIndex: Int, + override val endIndex: Int + ) : Token() - /** - * Parses the next one or more identifiers and returns the first one, caching the others, or - * returns null if the immediate next token is not an identifier. - */ - private fun parseIdentifiersAndReturnFirst(): Token? { - parsedIdentifierCache += parseIdentifiers() ?: return null - return retrieveNextParsedIdentifier() - } + class MinusSymbol( + override val startIndex: Int, override val endIndex: Int + ) : Token(), UnaryOperatorToken, BinaryOperatorToken { + override fun getUnaryOperator(): MathUnaryOperation.Operator = NEGATE - /** - * Returns the next identifier tokens or null if the next character itself is not a token, or - * does not indicate one or more following identifier tokens. Must not be called at the end of - * the stream. - */ - private fun parseIdentifiers(): List? { - val parsedIndex = currentIndex - val nextNonIdentifierIndex = seekUntil { it.isNotLetter() } - return when (nextNonIdentifierIndex - parsedIndex) { - // The next character is something other than a potential identifier. - 0 -> null - // Trivial case: there's a single letter identifier. - 1 -> listOf(parseSingleLetterIdentifier()) - // Complex case: either this is one multi-letter identifier, multiple single-letter - // identifiers with implied multiplication, or an invalid multi-letter identifier. - else -> - parseValidMultiLetterIdentifier(nextNonIdentifierIndex) - ?: parseMultipleSingleLetterIdentifiers(nextNonIdentifierIndex) - ?: listOf(parseInvalidMultiLetterIdentifier(nextNonIdentifierIndex)) - } + override fun getBinaryOperator(): MathBinaryOperation.Operator = SUBTRACT } - /** - * Returns the next token of the buffer as a single-letter identifier, or an invalid - * identifier if the character does not correspond to an allowed single-letter identifier. - */ - private fun parseSingleLetterIdentifier(): Token { - val parsedIndex = currentIndex - val potentialIdentifier = peekCharacter() - skipToken() - return maybeParseSingleLetterIdentifier(parsedIndex) - ?: Token.InvalidIdentifier(source, parsedIndex, potentialIdentifier.toString()) - } + class SquareRootSymbol(override val startIndex: Int, override val endIndex: Int) : Token() - /** - * Returns the next token of the buffer as a single-letter identifier, null if the character - * is not a valid single-letter identifier. Note that this does not change the underlying - * stream state (i.e. it does not skip the parsed token)--the caller is expected to do that. - * The caller is also expected to guarantee that the provided parsedIndex is within the - * buffer. - */ - private fun maybeParseSingleLetterIdentifier(parsedIndex: Int): Token? { - val potentialIdentifier = buffer[parsedIndex] - return if (potentialIdentifier in singleLetterIdentifiers) { - Token.Identifier(source, parsedIndex, potentialIdentifier.toString()) - } else null - } + class PlusSymbol( + override val startIndex: Int, override val endIndex: Int + ) : Token(), UnaryOperatorToken, BinaryOperatorToken { + override fun getUnaryOperator(): MathUnaryOperation.Operator = POSITIVE - /** - * Returns the next set of characters up to nextNonIdentifierIndex as a multi-letter - * identifier, or null if those characters do not correspond to a valid multi-letter - * identifier. Note that the returned list will always contain a single token corresponding to - * the multi-letter identifier, or the whole list will be null if parsing failed. - */ - private fun parseValidMultiLetterIdentifier(nextNonIdentifierIndex: Int): List? { - val parsedIndex = currentIndex - val potentialIdentifier = extractSubBufferString( - startIndex = currentIndex, - endIndex = nextNonIdentifierIndex - ) - return if (potentialIdentifier in multiLetterIdentifiers) { - advanceIndexTo(nextNonIdentifierIndex) - listOf(Token.Identifier(source, parsedIndex, potentialIdentifier)) - } else null + override fun getBinaryOperator(): MathBinaryOperation.Operator = ADD } - /** - * Returns a list of single-letter identifiers for all characters up to the specified index, - * or null if any characters encountered are not valid single-letter identifiers. - */ - private fun parseMultipleSingleLetterIdentifiers(nextNonIdentifierIndex: Int): List? { - val singleLetterIdentifiers = mutableListOf() - for (parsedIndex in currentIndex until nextNonIdentifierIndex) { - singleLetterIdentifiers += maybeParseSingleLetterIdentifier(parsedIndex) ?: return null - } - // Skip all of the characters encountered if each one corresponds to a valid identifier. - advanceIndexTo(nextNonIdentifierIndex) - return singleLetterIdentifiers + class MultiplySymbol( + override val startIndex: Int, override val endIndex: Int + ) : Token(), BinaryOperatorToken { + override fun getBinaryOperator(): MathBinaryOperation.Operator = MULTIPLY } - /** - * Returns a token indicating that all characters from the current index to the specified - * index (but not including that index) correspond to an invalid multi-letter identifier. - */ - private fun parseInvalidMultiLetterIdentifier(nextNonIdentifierIndex: Int): Token { - // Assume all characters between currentIndex and nextNonIdentifierIndex (exclusive) - // comprise a single, unknown multi-letter identifier. - val parsedIndex = currentIndex - advanceIndexTo(nextNonIdentifierIndex) - return Token.InvalidIdentifier( - source, - parsedIndex, - extractSubBufferString(startIndex = parsedIndex, endIndex = nextNonIdentifierIndex) - ) + class DivideSymbol( + override val startIndex: Int, override val endIndex: Int + ) : Token(), BinaryOperatorToken { + override fun getBinaryOperator(): MathBinaryOperation.Operator = DIVIDE } - /** - * Returns the next parenthesis token (open or close) or null if the next token is not a - * parenthesis. Must not be called at the end of the stream. - */ - private fun parseParenthesis(): Token? { - val parsedIndex = currentIndex - return when (peekCharacter()) { - LEFT_PARENTHESIS -> { - skipToken() - Token.OpenParenthesis(source, parsedIndex) - } - RIGHT_PARENTHESIS -> { - skipToken() - Token.CloseParenthesis(source, parsedIndex) - } - else -> null - } + class ExponentiationSymbol( + override val startIndex: Int, override val endIndex: Int + ) : Token(), BinaryOperatorToken { + override fun getBinaryOperator(): MathBinaryOperation.Operator = EXPONENTIATE } - /** - * Returns an invalid token for the next character in the stream. Must not be called at the - * end of the stream. - */ - private fun parseInvalidToken(): Token { - val parsedIndex = currentIndex - val errorCharacter = peekCharacter() - // Skip the error token to try and recover to continue tokenizing. - skipToken() - return Token.InvalidToken(source, parsedIndex, errorCharacter.toString()) - } + class EqualsSymbol(override val startIndex: Int, override val endIndex: Int) : Token() - /** - * Returns the next character in the buffer. Should only be called when not at the end of the - * stream. - */ - private fun peekCharacter(): Char = buffer[currentIndex] - - /** - * Returns a string representation of a cut of the stream buffer starting at the specified - * index and up to, but not including, the specified end index. It's assumed the caller - * ensures that 0 <= startIndex <= endIndex < buffer.size. - */ - private fun extractSubBufferString(startIndex: Int, endIndex: Int): String { - return String(chars = buffer, offset = startIndex, length = endIndex - startIndex) - } + class LeftParenthesisSymbol( + override val startIndex: Int, override val endIndex: Int + ) : Token() - /** Returns whether the specified index as at the end of the stream. */ - private fun isEofIndex(index: Int): Boolean = index == buffer.size + class RightParenthesisSymbol( + override val startIndex: Int, override val endIndex: Int + ) : Token() - /** - * Advances the current index to the specified index (must be bigger than or equal to current - * index) and should not exceed the length of the stream. - */ - private fun advanceIndexTo(newIndex: Int) { - currentIndex = newIndex.coerceIn(currentIndex..buffer.size) - } + class IncompleteFunctionName( + override val startIndex: Int, override val endIndex: Int + ) : Token() - /** Skips the next token in the stream. */ - private fun skipToken() = advanceIndexTo(currentIndex + 1) - - /** - * Returns the index of the first character not matching the specified predicate, or the - * stream length if the rest of the stream matches. - */ - private fun seekUntil(startIndex: Int = currentIndex, predicate: (Char) -> Boolean): Int { - var advanceIndex = startIndex - while (!isEofIndex(advanceIndex) && !predicate(buffer[advanceIndex])) advanceIndex++ - return advanceIndex - } + class InvalidToken(override val startIndex: Int, override val endIndex: Int) : Token() + } - /** - * Returns the first index not matching the specified list, or the stream length if the rest - * of the stream matches. - */ - private fun seekUntilCharacterNotFound( - matchingChars: List, - startIndex: Int = currentIndex - ): Int = seekUntil(startIndex) { it !in matchingChars } + // TODO: consider whether to use the more correct & expensive Java Char.isWhitespace(). + private fun Char.isWhitespace(): Boolean = when (this) { + ' ', '\t', '\n', '\r' -> true + else -> false + } + + private fun PeekableIterator.consumeWhitespace() { + while (peek()?.isWhitespace() == true) next() } - } -} -private fun Char.isNotLetter(): Boolean { - return !isLetter() + /** + * Expects each of the characters to be next in the token stream, in the order of the string. + * All characters must be present in [this] iterator. Returns non-null if a failure occurs, + * otherwise null if all characters were confirmed to be present. If null is returned, [this] + * iterator will be at the token that comes after the last confirmed character in the string. + */ + private fun PeekableIterator.expectNextCharsForFunctionName( + chars: String, startIndex: Int + ): Token? { + for (c in chars) { + expectNextValue { c } + ?: return Token.IncompleteFunctionName(startIndex, endIndex = getRetrievalCount()) + } + return null + } + } } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt deleted file mode 100644 index 9f9ba15934d..00000000000 --- a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer2.kt +++ /dev/null @@ -1,359 +0,0 @@ -package org.oppia.android.util.math - -import java.lang.StringBuilder -import org.oppia.android.app.model.MathBinaryOperation -import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD -import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE -import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE -import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY -import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT -import org.oppia.android.app.model.MathUnaryOperation -import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE -import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE - -// TODO: rename to MathTokenizer & add documentation. -// TODO: consider a more efficient implementation that uses 1 underlying buffer (which could still -// be sequence-backed) with a forced lookahead-of-1 API, to also avoid rebuffering when parsing -// sequences of characters like for integers. - -// TODO: add customization to omit certain symbols (such as variables for numeric expressions?) -class MathTokenizer2 private constructor() { - companion object { - fun tokenize(input: String): Sequence = tokenize(input.toCharArray().asSequence()) - - fun tokenize(input: Sequence): Sequence { - val chars = PeekableIterator.fromSequence(input) - return generateSequence { - // Consume any whitespace that might precede a valid token. - chars.consumeWhitespace() - - // Parse the next token from the underlying sequence. - when (chars.peek()) { - in '0'..'9' -> tokenizeIntegerOrRealNumber(chars) - in 'a'..'z', in 'A'..'Z' -> tokenizeVariableOrFunctionName(chars) - '√' -> tokenizeSymbol(chars) { startIndex, endIndex -> - Token.SquareRootSymbol(startIndex, endIndex) - } - '+' -> tokenizeSymbol(chars) { startIndex, endIndex -> - Token.PlusSymbol(startIndex, endIndex) - } - // TODO: add tests for different subtraction/minus symbols. - '-', '−' -> tokenizeSymbol(chars) { startIndex, endIndex -> - Token.MinusSymbol(startIndex, endIndex) - } - '*', '×' -> tokenizeSymbol(chars) { startIndex, endIndex -> - Token.MultiplySymbol(startIndex, endIndex) - } - '/', '÷' -> tokenizeSymbol(chars) { startIndex, endIndex -> - Token.DivideSymbol(startIndex, endIndex) - } - '^' -> tokenizeSymbol(chars) { startIndex, endIndex -> - Token.ExponentiationSymbol(startIndex, endIndex) - } - '=' -> tokenizeSymbol(chars) { startIndex, endIndex -> - Token.EqualsSymbol(startIndex, endIndex) - } - '(' -> tokenizeSymbol(chars) { startIndex, endIndex -> - Token.LeftParenthesisSymbol(startIndex, endIndex) - } - ')' -> tokenizeSymbol(chars) { startIndex, endIndex -> - Token.RightParenthesisSymbol(startIndex, endIndex) - } - null -> null // End of stream. - // Invalid character. - else -> tokenizeSymbol(chars) { startIndex, endIndex -> - Token.InvalidToken(startIndex, endIndex) - } - } - } - } - - private fun tokenizeIntegerOrRealNumber(chars: PeekableIterator): Token { - val startIndex = chars.getRetrievalCount() - val integerPart1 = - parseInteger(chars) - ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) - chars.consumeWhitespace() // Whitespace is allowed between digits and the '.'. - return if (chars.peek() == '.') { - chars.next() // Parse the "." since it will be re-added later. - chars.consumeWhitespace() // Whitespace is allowed between the '.' and following digits. - - // Another integer must follow the ".". - val integerPart2 = parseInteger(chars) - ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) - - // TODO: validate that the result isn't NaN or INF. - val doubleValue = "$integerPart1.$integerPart2".toDoubleOrNull() - ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) - Token.PositiveRealNumber(doubleValue, startIndex, endIndex = chars.getRetrievalCount()) - } else { - Token.PositiveInteger( - integerPart1.toIntOrNull() - ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()), - startIndex, - endIndex = chars.getRetrievalCount() - ) - } - } - - private fun tokenizeVariableOrFunctionName(chars: PeekableIterator): Token { - val startIndex = chars.getRetrievalCount() - val firstChar = chars.next() - - // latin_letter = lowercase_latin_letter | uppercase_latin_letter ; - // variable = latin_letter ; - return tokenizeFunctionName(firstChar, startIndex, chars) - ?: Token.VariableName( - firstChar.toString(), startIndex, endIndex = chars.getRetrievalCount() - ) - } - - private fun tokenizeFunctionName( - currChar: Char, startIndex: Int, chars: PeekableIterator - ): Token? { - // allowed_function_name = "sqrt" ; - // disallowed_function_name = - // "exp" | "log" | "log10" | "ln" | "sin" | "cos" | "tan" | "cot" | "csc" - // | "sec" | "atan" | "asin" | "acos" | "abs" ; - // function_name = allowed_function_name | disallowed_function_name ; - val nextChar = chars.peek() - return when (currChar) { - 'a' -> { - // abs, acos, asin, atan, or variable. - when (nextChar) { - 'b' -> - tokenizeExpectedFunction(name = "abs", isAllowedFunction = false, startIndex, chars) - 'c' -> - tokenizeExpectedFunction(name = "acos", isAllowedFunction = false, startIndex, chars) - 's' -> - tokenizeExpectedFunction(name = "asin", isAllowedFunction = false, startIndex, chars) - 't' -> - tokenizeExpectedFunction(name = "atan", isAllowedFunction = false, startIndex, chars) - else -> null // Must be a variable. - } - } - 'c' -> { - // cos, cot, csc, or variable. - when (nextChar) { - 'o' -> { - chars.next() // Skip the 'o' to go to the last character. - val name = if (chars.peek() == 's') { - chars.expectNextMatches { it == 's' } - ?: return Token.IncompleteFunctionName( - startIndex, endIndex = chars.getRetrievalCount() - ) - "cos" - } else { - // Otherwise, it must be 'c' for 'cot' since the parser can't backtrack. - chars.expectNextMatches { it == 't' } - ?: return Token.IncompleteFunctionName( - startIndex, endIndex = chars.getRetrievalCount() - ) - "cot" - } - Token.FunctionName( - name, isAllowedFunction = false, startIndex, endIndex = chars.getRetrievalCount() - ) - } - 's' -> - tokenizeExpectedFunction(name = "csc", isAllowedFunction = false, startIndex, chars) - else -> null // Must be a variable. - } - } - 'e' -> { - // exp or variable. - if (nextChar == 'x') { - tokenizeExpectedFunction(name = "exp", isAllowedFunction = false, startIndex, chars) - } else null // Must be a variable. - } - 'l' -> { - // ln, log, log10, or variable. - when (nextChar) { - 'n' -> - tokenizeExpectedFunction(name = "ln", isAllowedFunction = false, startIndex, chars) - 'o' -> { - // Skip the 'o'. Following the 'o' must be a 'g' since the parser can't backtrack. - chars.next() - chars.expectNextMatches { it == 'g' } - ?: return Token.IncompleteFunctionName( - startIndex, endIndex = chars.getRetrievalCount() - ) - val name = if (chars.peek() == '1') { - // '10' must be next for 'log10'. - chars.expectNextMatches { it == '1' } - ?: return Token.IncompleteFunctionName( - startIndex, endIndex = chars.getRetrievalCount() - ) - chars.expectNextMatches { it == '0' } - ?: return Token.IncompleteFunctionName( - startIndex, endIndex = chars.getRetrievalCount() - ) - "log10" - } else "log" - Token.FunctionName( - name, isAllowedFunction = false, startIndex, endIndex = chars.getRetrievalCount() - ) - } - else -> null // Must be a variable. - } - } - 's' -> { - // sec, sin, sqrt, or variable. - when (nextChar) { - 'e' -> - tokenizeExpectedFunction(name = "sec", isAllowedFunction = false, startIndex, chars) - 'i' -> - tokenizeExpectedFunction(name = "sin", isAllowedFunction = false, startIndex, chars) - 'q' -> - tokenizeExpectedFunction(name = "sqrt", isAllowedFunction = true, startIndex, chars) - else -> null // Must be a variable. - } - } - 't' -> { - // tan or variable. - if (nextChar == 'a') { - tokenizeExpectedFunction(name = "tan", isAllowedFunction = false, startIndex, chars) - } else null // Must be a variable. - } - else -> null // Must be a variable since no known functions match the first character. - } - } - - private fun tokenizeExpectedFunction( - name: String, isAllowedFunction: Boolean, startIndex: Int, chars: PeekableIterator - ): Token { - return chars.expectNextCharsForFunctionName(name.substring(1), startIndex) - ?: Token.FunctionName( - name, isAllowedFunction, startIndex, endIndex = chars.getRetrievalCount() - ) - } - - private fun tokenizeSymbol(chars: PeekableIterator, factory: (Int, Int) -> Token): Token { - val startIndex = chars.getRetrievalCount() - chars.next() // Parse the symbol. - val endIndex = chars.getRetrievalCount() - return factory(startIndex, endIndex) - } - - private fun parseInteger(chars: PeekableIterator): String? { - val integerBuilder = StringBuilder() - while (chars.peek() in '0'..'9') { - integerBuilder.append(chars.next()) - } - return if (integerBuilder.isNotEmpty()) { - integerBuilder.toString() - } else null // Failed to parse; no digits. - } - - interface UnaryOperatorToken { - fun getUnaryOperator(): MathUnaryOperation.Operator - } - - interface BinaryOperatorToken { - fun getBinaryOperator(): MathBinaryOperation.Operator - } - - sealed class Token { - /** The index in the input stream at which point this token begins. */ - abstract val startIndex: Int - - /** The (exclusive) index in the input stream at which point this token ends. */ - abstract val endIndex: Int - - class PositiveInteger( - val parsedValue: Int, override val startIndex: Int, override val endIndex: Int - ) : Token() - - class PositiveRealNumber( - val parsedValue: Double, override val startIndex: Int, override val endIndex: Int - ) : Token() - - class VariableName( - val parsedName: String, override val startIndex: Int, override val endIndex: Int - ) : Token() - - class FunctionName( - val parsedName: String, val isAllowedFunction: Boolean, override val startIndex: Int, - override val endIndex: Int - ) : Token() - - class MinusSymbol( - override val startIndex: Int, override val endIndex: Int - ) : Token(), UnaryOperatorToken, BinaryOperatorToken { - override fun getUnaryOperator(): MathUnaryOperation.Operator = NEGATE - - override fun getBinaryOperator(): MathBinaryOperation.Operator = SUBTRACT - } - - class SquareRootSymbol(override val startIndex: Int, override val endIndex: Int) : Token() - - class PlusSymbol( - override val startIndex: Int, override val endIndex: Int - ) : Token(), UnaryOperatorToken, BinaryOperatorToken { - override fun getUnaryOperator(): MathUnaryOperation.Operator = POSITIVE - - override fun getBinaryOperator(): MathBinaryOperation.Operator = ADD - } - - class MultiplySymbol( - override val startIndex: Int, override val endIndex: Int - ) : Token(), BinaryOperatorToken { - override fun getBinaryOperator(): MathBinaryOperation.Operator = MULTIPLY - } - - class DivideSymbol( - override val startIndex: Int, override val endIndex: Int - ) : Token(), BinaryOperatorToken { - override fun getBinaryOperator(): MathBinaryOperation.Operator = DIVIDE - } - - class ExponentiationSymbol( - override val startIndex: Int, override val endIndex: Int - ) : Token(), BinaryOperatorToken { - override fun getBinaryOperator(): MathBinaryOperation.Operator = EXPONENTIATE - } - - class EqualsSymbol(override val startIndex: Int, override val endIndex: Int) : Token() - - class LeftParenthesisSymbol( - override val startIndex: Int, override val endIndex: Int - ) : Token() - - class RightParenthesisSymbol( - override val startIndex: Int, override val endIndex: Int - ) : Token() - - class IncompleteFunctionName( - override val startIndex: Int, override val endIndex: Int - ) : Token() - - class InvalidToken(override val startIndex: Int, override val endIndex: Int) : Token() - } - - // TODO: consider whether to use the more correct & expensive Java Char.isWhitespace(). - private fun Char.isWhitespace(): Boolean = when (this) { - ' ', '\t', '\n', '\r' -> true - else -> false - } - - private fun PeekableIterator.consumeWhitespace() { - while (peek()?.isWhitespace() == true) next() - } - - /** - * Expects each of the characters to be next in the token stream, in the order of the string. - * All characters must be present in [this] iterator. Returns non-null if a failure occurs, - * otherwise null if all characters were confirmed to be present. If null is returned, [this] - * iterator will be at the token that comes after the last confirmed character in the string. - */ - private fun PeekableIterator.expectNextCharsForFunctionName( - chars: String, startIndex: Int - ): Token? { - for (c in chars) { - expectNextValue { c } - ?: return Token.IncompleteFunctionName(startIndex, endIndex = getRetrievalCount()) - } - return null - } - } -} diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt deleted file mode 100644 index 8423f072745..00000000000 --- a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionParser.kt +++ /dev/null @@ -1,1038 +0,0 @@ -package org.oppia.android.util.math - -import kotlin.math.absoluteValue -import org.oppia.android.app.model.MathBinaryOperation -import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD -import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE -import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE -import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY -import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT -import org.oppia.android.app.model.MathEquation -import org.oppia.android.app.model.MathExpression -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE -import org.oppia.android.app.model.MathFunctionCall -import org.oppia.android.app.model.MathUnaryOperation -import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE -import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE -import org.oppia.android.app.model.Real -import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError -import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError -import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError -import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError -import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError -import org.oppia.android.util.math.MathParsingError.FunctionNameIncompleteError -import org.oppia.android.util.math.MathParsingError.GenericError -import org.oppia.android.util.math.MathParsingError.HangingSquareRootError -import org.oppia.android.util.math.MathParsingError.InvalidFunctionInUseError -import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError -import org.oppia.android.util.math.MathParsingError.NestedExponentsError -import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberAfterBinaryOperatorError -import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberBeforeBinaryOperatorError -import org.oppia.android.util.math.MathParsingError.NumberAfterVariableError -import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError -import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError -import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError -import org.oppia.android.util.math.MathParsingError.SubsequentBinaryOperatorsError -import org.oppia.android.util.math.MathParsingError.SubsequentUnaryOperatorsError -import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError -import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError -import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError -import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError -import org.oppia.android.util.math.MathTokenizer2.Companion.Token -import org.oppia.android.util.math.MathTokenizer2.Companion.Token.DivideSymbol -import org.oppia.android.util.math.MathTokenizer2.Companion.Token.EqualsSymbol -import org.oppia.android.util.math.MathTokenizer2.Companion.Token.ExponentiationSymbol -import org.oppia.android.util.math.MathTokenizer2.Companion.Token.FunctionName -import org.oppia.android.util.math.MathTokenizer2.Companion.Token.IncompleteFunctionName -import org.oppia.android.util.math.MathTokenizer2.Companion.Token.InvalidToken -import org.oppia.android.util.math.MathTokenizer2.Companion.Token.LeftParenthesisSymbol -import org.oppia.android.util.math.MathTokenizer2.Companion.Token.MinusSymbol -import org.oppia.android.util.math.MathTokenizer2.Companion.Token.MultiplySymbol -import org.oppia.android.util.math.MathTokenizer2.Companion.Token.PlusSymbol -import org.oppia.android.util.math.MathTokenizer2.Companion.Token.PositiveInteger -import org.oppia.android.util.math.MathTokenizer2.Companion.Token.PositiveRealNumber -import org.oppia.android.util.math.MathTokenizer2.Companion.Token.RightParenthesisSymbol -import org.oppia.android.util.math.MathTokenizer2.Companion.Token.SquareRootSymbol -import org.oppia.android.util.math.MathTokenizer2.Companion.Token.VariableName -import org.oppia.android.util.math.NumericExpressionParser.ParseContext.AlgebraicExpressionContext -import org.oppia.android.util.math.NumericExpressionParser.ParseContext.NumericExpressionContext - -class NumericExpressionParser private constructor(private val parseContext: ParseContext) { - // TODO: - // - Add helpers to reduce overall parser length. - // - Integrate with new errors & update the routines to not rely on exceptions except in actual exceptional cases. Make sure optional errors can be disabled (for testing purposes). - // - Rename this to be a generic parser, update the public API, add documentation, remove the old classes, and split up the big test routines into actual separate tests. - - // TODO: implement specific errors. - // TODO: verify remaining GenericErrors are correct. - - // TODO: document that 'generic' means either 'numeric' or 'algebraic' (ie that the expression is syntactically the same between both grammars). - // TODO: document that one design goal is keeping the grammar for this parser as LL(1) & why. - - private fun parseGenericEquationGrammar(): MathParsingResult { - // generic_equation_grammar = generic_equation ; - return parseGenericEquation().maybeFail { equation -> - checkForLearnerErrors(equation.leftSide) ?: checkForLearnerErrors(equation.rightSide) - } - } - - private fun parseGenericExpressionGrammar(): MathParsingResult { - // generic_expression_grammar = generic_expression ; - return parseGenericExpression().maybeFail { expression -> checkForLearnerErrors(expression) } - } - - private fun parseGenericEquation(): MathParsingResult { - // algebraic_equation = generic_expression , equals_operator , generic_expression ; - - if (parseContext.hasNextTokenOfType()) { - // If equals starts the string, then there's no LHS. - return EquationMissingLhsOrRhsError.toFailure() - } - - val lhsResult = parseGenericExpression().also { - parseContext.consumeTokenOfType() - }.maybeFail { - if (!parseContext.hasMoreTokens()) { - // If there are no tokens following the equals symbol, then there's no RHS. - EquationMissingLhsOrRhsError - } else null - } - - val rhsResult = lhsResult.flatMap { parseGenericExpression() } - return lhsResult.combineWith(rhsResult) { lhs, rhs -> - MathEquation.newBuilder().apply { - leftSide = lhs - rightSide = rhs - }.build() - } - } - - private fun parseGenericExpression(): MathParsingResult { - // generic_expression = generic_add_sub_expression ; - return parseGenericAddSubExpression() - } - - private fun parseGenericAddSubExpression(): MathParsingResult { - // generic_add_sub_expression = - // generic_mult_div_expression , { generic_add_sub_expression_rhs } ; - return parseGenericBinaryExpression( - parseLhs = this::parseGenericMultDivExpression - ) { nextToken -> - // generic_add_sub_expression_rhs = - // generic_add_expression_rhs | generic_sub_expression_rhs ; - when (nextToken) { - is PlusSymbol -> BinaryOperationRhs( - operator = ADD, - rhsResult = parseGenericAddExpressionRhs() - ) - is MinusSymbol -> BinaryOperationRhs( - operator = SUBTRACT, - rhsResult = parseGenericSubExpressionRhs() - ) - is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, - is ExponentiationSymbol, is FunctionName, is InvalidToken, is LeftParenthesisSymbol, - is MultiplySymbol, is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, - is IncompleteFunctionName, null -> null - } - } - } - - private fun parseGenericAddExpressionRhs(): MathParsingResult { - // generic_add_expression_rhs = plus_operator , generic_mult_div_expression ; - return parseContext.consumeTokenOfType().maybeFail { - if (!parseContext.hasMoreTokens()) { - NoVariableOrNumberAfterBinaryOperatorError(ADD) - } else null - }.flatMap { - parseGenericMultDivExpression() - } - } - - private fun parseGenericSubExpressionRhs(): MathParsingResult { - // generic_sub_expression_rhs = minus_operator , generic_mult_div_expression ; - return parseContext.consumeTokenOfType().maybeFail { - if (!parseContext.hasMoreTokens()) { - NoVariableOrNumberAfterBinaryOperatorError(SUBTRACT) - } else null - }.flatMap { - parseGenericMultDivExpression() - } - } - - private fun parseGenericMultDivExpression(): MathParsingResult { - // generic_mult_div_expression = - // generic_exp_expression , { generic_mult_div_expression_rhs } ; - return parseGenericBinaryExpression( - parseLhs = this::parseGenericExpExpression - ) { nextToken -> - // generic_mult_div_expression_rhs = - // generic_mult_expression_rhs - // | generic_div_expression_rhs - // | generic_implicit_mult_expression_rhs ; - when (nextToken) { - is MultiplySymbol -> BinaryOperationRhs( - operator = MULTIPLY, - rhsResult = parseGenericMultExpressionRhs() - ) - is DivideSymbol -> BinaryOperationRhs( - operator = DIVIDE, - rhsResult = parseGenericDivExpressionRhs() - ) - is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> BinaryOperationRhs( - operator = MULTIPLY, - rhsResult = parseGenericImplicitMultExpressionRhs(), - isImplicit = true - ) - is VariableName -> { - if (parseContext is AlgebraicExpressionContext) { - BinaryOperationRhs( - operator = MULTIPLY, - rhsResult = parseGenericImplicitMultExpressionRhs(), - isImplicit = true - ) - } else null - } - // Not a match to the expression. - is PositiveInteger, is PositiveRealNumber, is EqualsSymbol, is ExponentiationSymbol, - is InvalidToken, is MinusSymbol, is PlusSymbol, is RightParenthesisSymbol, - is IncompleteFunctionName, null -> null - } - } - } - - private fun parseGenericMultExpressionRhs(): MathParsingResult { - // generic_mult_expression_rhs = multiplication_operator , generic_exp_expression ; - return parseContext.consumeTokenOfType().maybeFail { - if (!parseContext.hasMoreTokens()) { - NoVariableOrNumberAfterBinaryOperatorError(MULTIPLY) - } else null - }.flatMap { - parseGenericExpExpression() - } - } - - private fun parseGenericDivExpressionRhs(): MathParsingResult { - // generic_div_expression_rhs = division_operator , generic_exp_expression ; - return parseContext.consumeTokenOfType().maybeFail { - if (!parseContext.hasMoreTokens()) { - NoVariableOrNumberAfterBinaryOperatorError(DIVIDE) - } else null - }.flatMap { - parseGenericExpExpression() - } - } - - private fun parseGenericImplicitMultExpressionRhs(): MathParsingResult { - // generic_implicit_mult_expression_rhs is either numeric_implicit_mult_expression_rhs or - // algebraic_implicit_mult_or_exp_expression_rhs depending on the current parser context. - return when (parseContext) { - is NumericExpressionContext -> parseNumericImplicitMultExpressionRhs() - is AlgebraicExpressionContext -> parseAlgebraicImplicitMultOrExpExpressionRhs() - } - } - - private fun parseNumericImplicitMultExpressionRhs(): MathParsingResult { - // numeric_implicit_mult_expression_rhs = generic_term_without_unary_without_number ; - return parseGenericTermWithoutUnaryWithoutNumber() - } - - private fun parseAlgebraicImplicitMultOrExpExpressionRhs(): MathParsingResult { - // algebraic_implicit_mult_or_exp_expression_rhs = - // generic_term_without_unary_without_number , [ generic_exp_expression_tail ] ; - val possibleLhs = parseGenericTermWithoutUnaryWithoutNumber() - return if (parseContext.hasNextTokenOfType()) { - parseGenericExpExpressionTail(possibleLhs) - } else possibleLhs - } - - private fun parseGenericExpExpression(): MathParsingResult { - // generic_exp_expression = generic_term_with_unary , [ generic_exp_expression_tail ] ; - val possibleLhs = parseGenericTermWithUnary() - return if (parseContext.hasNextTokenOfType()) { - parseGenericExpExpressionTail(possibleLhs) - } else possibleLhs - } - - // Use tail recursion so that the last exponentiation is evaluated first, and right-to-left - // associativity can be kept via backtracking. - private fun parseGenericExpExpressionTail( - lhsResult: MathParsingResult - ): MathParsingResult { - // generic_exp_expression_tail = exponentiation_operator , generic_exp_expression ; - return BinaryOperationRhs( - operator = EXPONENTIATE, - rhsResult = lhsResult.flatMap { - parseContext.consumeTokenOfType() - }.maybeFail { - if (!parseContext.hasMoreTokens()) { - NoVariableOrNumberAfterBinaryOperatorError(EXPONENTIATE) - } else null - }.flatMap { - parseGenericExpExpression() - } - ).computeBinaryOperationExpression(lhsResult) - } - - private fun parseGenericTermWithUnary(): MathParsingResult { - // generic_term_with_unary = - // number | generic_term_without_unary_without_number | generic_plus_minus_unary_term ; - return when (val nextToken = parseContext.peekToken()) { - is MinusSymbol, is PlusSymbol -> parseGenericPlusMinusUnaryTerm() - is PositiveInteger, is PositiveRealNumber -> parseNumber().takeUnless { - parseContext.hasNextTokenOfType() - || parseContext.hasNextTokenOfType() - } ?: SpacesBetweenNumbersError.toFailure() - is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> - parseGenericTermWithoutUnaryWithoutNumber() - is VariableName -> { - if (parseContext is AlgebraicExpressionContext) { - parseGenericTermWithoutUnaryWithoutNumber() - } else VariableInNumericExpressionError.toFailure() - } - is DivideSymbol, is ExponentiationSymbol, is MultiplySymbol -> { - val previousToken = parseContext.getPreviousToken() - when { - previousToken is MathTokenizer2.Companion.BinaryOperatorToken -> { - SubsequentBinaryOperatorsError( - operator1 = parseContext.extractSubexpression(previousToken), - operator2 = parseContext.extractSubexpression(nextToken) - ).toFailure() - } - nextToken is MathTokenizer2.Companion.BinaryOperatorToken -> { - NoVariableOrNumberBeforeBinaryOperatorError( - operator = nextToken.getBinaryOperator() - ).toFailure() - } - else -> GenericError.toFailure() - } - } - is EqualsSymbol -> { - if (parseContext is AlgebraicExpressionContext && parseContext.isPartOfEquation) { - EquationHasWrongNumberOfEqualsError.toFailure() - } else GenericError.toFailure() - } - is IncompleteFunctionName -> nextToken.toFailure() - is InvalidToken -> nextToken.toFailure() - is RightParenthesisSymbol, null -> GenericError.toFailure() - } - } - - private fun parseGenericTermWithoutUnaryWithoutNumber(): MathParsingResult { - // generic_term_without_unary_without_number is either numeric_term_without_unary_without_number - // or algebraic_term_without_unary_without_number based the current parser context. - return when (parseContext) { - is NumericExpressionContext -> parseNumericTermWithoutUnaryWithoutNumber() - is AlgebraicExpressionContext -> parseAlgebraicTermWithoutUnaryWithoutNumber() - } - } - - private fun parseNumericTermWithoutUnaryWithoutNumber(): MathParsingResult { - // numeric_term_without_unary_without_number = - // generic_function_expression | generic_group_expression | generic_rooted_term ; - return when (val nextToken = parseContext.peekToken()) { - is FunctionName -> parseGenericFunctionExpression() - is LeftParenthesisSymbol -> parseGenericGroupExpression() - is SquareRootSymbol -> parseGenericRootedTerm() - is VariableName -> VariableInNumericExpressionError.toFailure() - is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, - is ExponentiationSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, - is RightParenthesisSymbol, null -> GenericError.toFailure() - is IncompleteFunctionName -> nextToken.toFailure() - is InvalidToken -> nextToken.toFailure() - } - } - - private fun parseAlgebraicTermWithoutUnaryWithoutNumber(): MathParsingResult { - // algebraic_term_without_unary_without_number = - // generic_function_expression | generic_group_expression | generic_rooted_term | variable ; - return when (val nextToken = parseContext.peekToken()) { - is FunctionName -> parseGenericFunctionExpression() - is LeftParenthesisSymbol -> parseGenericGroupExpression() - is SquareRootSymbol -> parseGenericRootedTerm() - is VariableName -> parseVariable() - is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, - is ExponentiationSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, - is RightParenthesisSymbol, null -> GenericError.toFailure() - is IncompleteFunctionName -> nextToken.toFailure() - is InvalidToken -> nextToken.toFailure() - } - } - - private fun parseGenericFunctionExpression(): MathParsingResult { - // generic_function_expression = function_name , left_paren , generic_expression , right_paren ; - val funcNameResult = - parseContext.consumeTokenOfType().maybeFail { functionName -> - when { - !functionName.isAllowedFunction -> InvalidFunctionInUseError(functionName.parsedName) - functionName.parsedName == "sqrt" -> null - else -> GenericError - } - }.also { - parseContext.consumeTokenOfType() - } - val argResult = funcNameResult.flatMap { parseGenericExpression() } - val rightParenResult = - argResult.flatMap { - parseContext.consumeTokenOfType { UnbalancedParenthesesError } - } - return funcNameResult.combineWith(argResult, rightParenResult) { funcName, arg, rightParen -> - MathExpression.newBuilder().apply { - parseStartIndex = funcName.startIndex - parseEndIndex = rightParen.endIndex - functionCall = MathFunctionCall.newBuilder().apply { - functionType = MathFunctionCall.FunctionType.SQUARE_ROOT - argument = arg - }.build() - }.build() - } - } - - private fun parseGenericGroupExpression(): MathParsingResult { - // generic_group_expression = left_paren , generic_expression , right_paren ; - val leftParenResult = parseContext.consumeTokenOfType() - val expResult = - leftParenResult.flatMap { - if (parseContext.hasMoreTokens()) { - parseGenericExpression() - } else UnbalancedParenthesesError.toFailure() - } - val rightParenResult = - expResult.flatMap { - parseContext.consumeTokenOfType { UnbalancedParenthesesError } - } - return leftParenResult.combineWith(expResult, rightParenResult) { leftParen, exp, rightParen -> - MathExpression.newBuilder().apply { - parseStartIndex = leftParen.startIndex - parseEndIndex = rightParen.endIndex - group = exp - }.build() - } - } - - private fun parseGenericPlusMinusUnaryTerm(): MathParsingResult { - // generic_plus_minus_unary_term = generic_negated_term | generic_positive_term ; - return when (val nextToken = parseContext.peekToken()) { - is MinusSymbol -> parseGenericNegatedTerm() - is PlusSymbol -> parseGenericPositiveTerm() - is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, - is ExponentiationSymbol, is FunctionName, is LeftParenthesisSymbol, is MultiplySymbol, - is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, null -> - GenericError.toFailure() - is IncompleteFunctionName -> nextToken.toFailure() - is InvalidToken -> nextToken.toFailure() - } - } - - private fun parseGenericNegatedTerm(): MathParsingResult { - // generic_negated_term = minus_operator , generic_mult_div_expression ; - val minusResult = parseContext.consumeTokenOfType() - val expResult = minusResult.flatMap { parseGenericMultDivExpression() } - return minusResult.combineWith(expResult) { minus, op -> - MathExpression.newBuilder().apply { - parseStartIndex = minus.startIndex - parseEndIndex = op.parseEndIndex - unaryOperation = MathUnaryOperation.newBuilder().apply { - operator = NEGATE - operand = op - }.build() - }.build() - } - } - - private fun parseGenericPositiveTerm(): MathParsingResult { - // generic_positive_term = plus_operator , generic_mult_div_expression ; - val plusResult = parseContext.consumeTokenOfType() - val expResult = plusResult.flatMap { parseGenericMultDivExpression() } - return plusResult.combineWith(expResult) { plus, op -> - MathExpression.newBuilder().apply { - parseStartIndex = plus.startIndex - parseEndIndex = op.parseEndIndex - unaryOperation = MathUnaryOperation.newBuilder().apply { - operator = POSITIVE - operand = op - }.build() - }.build() - } - } - - private fun parseGenericRootedTerm(): MathParsingResult { - // generic_rooted_term = square_root_operator , generic_term_with_unary ; - val sqrtResult = - parseContext.consumeTokenOfType().maybeFail { - if (!parseContext.hasMoreTokens()) HangingSquareRootError else null - } - val expResult = sqrtResult.flatMap { parseGenericTermWithUnary() } - return sqrtResult.combineWith(expResult) { sqrtSymbol, op -> - MathExpression.newBuilder().apply { - parseStartIndex = sqrtSymbol.startIndex - parseEndIndex = op.parseEndIndex - functionCall = MathFunctionCall.newBuilder().apply { - functionType = MathFunctionCall.FunctionType.SQUARE_ROOT - argument = op - }.build() - }.build() - } - } - - private fun parseNumber(): MathParsingResult { - // number = positive_real_number | positive_integer ; - return when (val nextToken = parseContext.peekToken()) { - is PositiveInteger -> { - parseContext.consumeTokenOfType().map { positiveInteger -> - MathExpression.newBuilder().apply { - parseStartIndex = positiveInteger.startIndex - parseEndIndex = positiveInteger.endIndex - constant = positiveInteger.toReal() - }.build() - } - } - is PositiveRealNumber -> { - parseContext.consumeTokenOfType().map { positiveRealNumber -> - MathExpression.newBuilder().apply { - parseStartIndex = positiveRealNumber.startIndex - parseEndIndex = positiveRealNumber.endIndex - constant = positiveRealNumber.toReal() - }.build() - } - } - is DivideSymbol, is EqualsSymbol, is ExponentiationSymbol, is FunctionName, - is LeftParenthesisSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, - is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, null -> - GenericError.toFailure() - is IncompleteFunctionName -> nextToken.toFailure() - is InvalidToken -> nextToken.toFailure() - } - } - - private fun parseVariable(): MathParsingResult { - val variableNameResult = - parseContext.consumeTokenOfType().maybeFail { - if (!parseContext.allowsVariables()) GenericError else null - }.maybeFail { variableName -> - return@maybeFail if (parseContext.hasMoreTokens()) { - when (val nextToken = parseContext.peekToken()) { - is PositiveInteger -> - NumberAfterVariableError(nextToken.toReal(), variableName.parsedName) - is PositiveRealNumber -> - NumberAfterVariableError(nextToken.toReal(), variableName.parsedName) - else -> null - } - } else null - } - return variableNameResult.map { variableName -> - MathExpression.newBuilder().apply { - parseStartIndex = variableName.startIndex - parseEndIndex = variableName.endIndex - variable = variableName.parsedName - }.build() - } - } - - private fun parseGenericBinaryExpression( - parseLhs: () -> MathParsingResult, parseRhs: (Token?) -> BinaryOperationRhs? - ): MathParsingResult { - var lastLhsResult = parseLhs() - while (!lastLhsResult.isFailure()) { - // Compute the next LHS if there are further RHS expressions. - lastLhsResult = - parseRhs(parseContext.peekToken()) - ?.computeBinaryOperationExpression(lastLhsResult) - ?: break // Not a match to the expression. - } - return lastLhsResult - } - - private fun checkForLearnerErrors(expression: MathExpression): MathParsingError? { - val firstMultiRedundantGroup = expression.findFirstMultiRedundantGroup() - val nextRedundantGroup = expression.findNextRedundantGroup() - val nextUnaryOperation = expression.findNextRedundantUnaryOperation() - val nextExpWithVariableExp = expression.findNextExponentiationWithVariablePower() - val nextExpWithTooLargePower = expression.findNextExponentiationWithTooLargePower() - val nextExpWithNestedExp = expression.findNextNestedExponentiation() - val nextDivByZero = expression.findNextDivisionByZero() - val disallowedVariables = expression.findAllDisallowedVariables(parseContext) - // Note that the order of checks here is important since errors have precedence, and some are - // redundant and, in the wrong order, may cause the wrong error to be returned. - val includeOptionalErrors = parseContext.errorCheckingMode.includesOptionalErrors() - return when { - includeOptionalErrors && firstMultiRedundantGroup != null -> { - val subExpression = parseContext.extractSubexpression(firstMultiRedundantGroup) - MultipleRedundantParenthesesError(subExpression, firstMultiRedundantGroup) - } - includeOptionalErrors && expression.expressionTypeCase == GROUP -> - SingleRedundantParenthesesError(parseContext.rawExpression, expression) - includeOptionalErrors && nextRedundantGroup != null -> { - val subExpression = parseContext.extractSubexpression(nextRedundantGroup) - RedundantParenthesesForIndividualTermsError(subExpression, nextRedundantGroup) - } - includeOptionalErrors && nextUnaryOperation != null -> SubsequentUnaryOperatorsError - includeOptionalErrors && nextExpWithVariableExp != null -> ExponentIsVariableExpressionError - includeOptionalErrors && nextExpWithTooLargePower != null -> ExponentTooLargeError - includeOptionalErrors && nextExpWithNestedExp != null -> NestedExponentsError - includeOptionalErrors && nextDivByZero != null -> TermDividedByZeroError - includeOptionalErrors && disallowedVariables.isNotEmpty() -> - DisabledVariablesInUseError(disallowedVariables.toList()) - else -> ensureNoRemainingTokens() - } - } - - private fun ensureNoRemainingTokens(): MathParsingError? { - // Make sure all tokens were consumed (otherwise there are trailing tokens which invalidate the - // whole grammar). - return if (parseContext.hasMoreTokens()) { - when (val nextToken = parseContext.peekToken()) { - is LeftParenthesisSymbol, is RightParenthesisSymbol -> UnbalancedParenthesesError - is EqualsSymbol -> { - if (parseContext is AlgebraicExpressionContext && parseContext.isPartOfEquation) { - EquationHasWrongNumberOfEqualsError - } else GenericError - } - is IncompleteFunctionName -> nextToken.toError() - is InvalidToken -> nextToken.toError() - is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is ExponentiationSymbol, - is FunctionName, is MinusSymbol, is MultiplySymbol, is PlusSymbol, is SquareRootSymbol, - is VariableName, null -> GenericError - } - } else null - } - - private fun PositiveInteger.toReal(): Real = Real.newBuilder().apply { - integer = parsedValue - }.build() - - private fun PositiveRealNumber.toReal(): Real = Real.newBuilder().apply { - irrational = parsedValue - }.build() - - @Suppress("unused") // The receiver is behaving as a namespace. - private fun IncompleteFunctionName.toError(): MathParsingError = FunctionNameIncompleteError - - private fun InvalidToken.toError(): MathParsingError = - UnnecessarySymbolsError(parseContext.extractSubexpression(this)) - - private fun IncompleteFunctionName.toFailure(): MathParsingResult = toError().toFailure() - - private fun InvalidToken.toFailure(): MathParsingResult = toError().toFailure() - - private sealed class ParseContext(val rawExpression: String) { - val tokens: PeekableIterator by lazy { - PeekableIterator.fromSequence(MathTokenizer2.tokenize(rawExpression)) - } - private var previousToken: Token? = null - - abstract val errorCheckingMode: ErrorCheckingMode - - abstract fun allowsVariables(): Boolean - - fun hasMoreTokens(): Boolean = tokens.hasNext() - - fun peekToken(): Token? = tokens.peek() - - /** - * Returns the last token consumed by [consumeTokenOfType], or null if none. Note: this should - * only be used for error reporting purposes, not for parsing. Using this for parsing would, in - * certain cases, allow for a non-LL(1) grammar which is against one design goal for this - * parser. - */ - fun getPreviousToken(): Token? = previousToken - - inline fun hasNextTokenOfType(): Boolean = peekToken() is T - - inline fun consumeTokenOfType( - missingError: () -> MathParsingError = { GenericError } - ): MathParsingResult { - val maybeToken = tokens.expectNextMatches { it is T } as? T - return maybeToken?.let { token -> - previousToken = token - MathParsingResult.Success(token) - } ?: missingError().toFailure() - } - - fun extractSubexpression(token: Token): String { - return rawExpression.substring(token.startIndex, token.endIndex) - } - - fun extractSubexpression(expression: MathExpression): String { - return rawExpression.substring(expression.parseStartIndex, expression.parseEndIndex) - } - - class NumericExpressionContext( - rawExpression: String, override val errorCheckingMode: ErrorCheckingMode - ) : ParseContext(rawExpression) { - // Numeric expressions never allow variables. - override fun allowsVariables(): Boolean = false - } - - class AlgebraicExpressionContext( - rawExpression: String, - val isPartOfEquation: Boolean, - private val allowedVariables: List, - override val errorCheckingMode: ErrorCheckingMode - ) : ParseContext(rawExpression) { - fun allowsVariable(variableName: String): Boolean = variableName in allowedVariables - - override fun allowsVariables(): Boolean = true - } - } - - companion object { - enum class ErrorCheckingMode { - REQUIRED_ONLY, - ALL_ERRORS - } - - sealed class MathParsingResult { - data class Success(val result: T) : MathParsingResult() - - data class Failure(val error: MathParsingError) : MathParsingResult() - } - - fun parseNumericExpression( - rawExpression: String, errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS - ): MathParsingResult = - createNumericParser(rawExpression, errorCheckingMode).parseGenericExpressionGrammar() - - fun parseAlgebraicExpression( - rawExpression: String, - allowedVariables: List, - errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS - ): MathParsingResult { - return createAlgebraicParser( - rawExpression, isPartOfEquation = false, allowedVariables, errorCheckingMode - ).parseGenericExpressionGrammar() - } - - fun parseAlgebraicEquation( - rawExpression: String, - allowedVariables: List, - errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS - ): MathParsingResult { - return createAlgebraicParser( - rawExpression, isPartOfEquation = true, allowedVariables, errorCheckingMode - ).parseGenericEquationGrammar() - } - - private fun createNumericParser( - rawExpression: String, errorCheckingMode: ErrorCheckingMode - ): NumericExpressionParser = - NumericExpressionParser(NumericExpressionContext(rawExpression, errorCheckingMode)) - - private fun createAlgebraicParser( - rawExpression: String, - isPartOfEquation: Boolean, - allowedVariables: List, - errorCheckingMode: ErrorCheckingMode - ): NumericExpressionParser { - return NumericExpressionParser( - AlgebraicExpressionContext( - rawExpression, isPartOfEquation, allowedVariables, errorCheckingMode - ) - ) - } - - private fun ErrorCheckingMode.includesOptionalErrors() = this == ErrorCheckingMode.ALL_ERRORS - - private fun MathParsingError.toFailure(): MathParsingResult = - MathParsingResult.Failure(this) - - private fun MathParsingResult.isFailure() = this is MathParsingResult.Failure - - /** - * Maps [this] result to a new value. Note that this lazily uses the provided function (i.e. - * it's only used if [this] result is passing, otherwise the method will short-circuit a failure - * state so that [this] result's failure is preserved). - * - * @param operation computes a new success result given the current successful result value - * @return a new [MathParsingResult] with a successful result provided by the operation, or the - * preserved failure of [this] result - */ - private fun MathParsingResult.map( - operation: (T1) -> T2 - ): MathParsingResult = flatMap { result -> MathParsingResult.Success(operation(result)) } - - /** - * Maps [this] result to a new value. Note that this lazily uses the provided function (i.e. - * it's only used if [this] result is passing, otherwise the method will short-circuit a failure - * state so that [this] result's failure is preserved). - * - * @param operation computes a new result (either a success or failure) given the current - * successful result value - * @return a new [MathParsingResult] with either a result provided by the operation, or the - * preserved failure of [this] result - */ - private fun MathParsingResult.flatMap( - operation: (T1) -> MathParsingResult - ): MathParsingResult { - return when (this) { - is MathParsingResult.Success -> operation(result) - is MathParsingResult.Failure -> error.toFailure() - } - } - - /** - * Potentially changes [this] result into a failure based on the provided [operation]. Note that - * this function lazily uses the operation (i.e. it's only called if [this] result is in a - * passing state), and the returned result will only be in a failing state if [operation] - * returns a non-null error. - * - * @param operation computes a failure error, or null if no error was determined, given the - * current successful result value - * @return either [this] or a failing result if [operation] was called & returned a non-null - * error - */ - private fun MathParsingResult.maybeFail( - operation: (T) -> MathParsingError? - ): MathParsingResult = flatMap { result -> operation(result)?.toFailure() ?: this } - - /** - * Calls an operation if [this] operation isn't already failing, and returns a failure only if - * that operation's result is a failure (otherwise returns [this] result). This function can be - * useful to ensure that subsequent operations are successful even when those operations' - * results are never directly used. - * - * @param operation computes a new result that, when failing, will result in a failing result - * returned from this function. This is only called if [this] result is currently - * successful. - * @return either [this] (iff either this result is failing, or the result of [operation] is a - * success), or the failure returned by [operation] - */ - private fun MathParsingResult.also( - operation: () -> MathParsingResult - ): MathParsingResult = flatMap { - when (val other = operation()) { - is MathParsingResult.Success -> this - is MathParsingResult.Failure -> other.error.toFailure() - } - } - - /** - * Combines [this] result with another result, given a specific combination function. - * - * @param other the result to combine with [this] result - * @param combine computes a new value given the result from [this] and [other]. Note that this - * is only called if both results are successful, and the corresponding successful values - * are provided in-order ([this] result's value is the first parameter, and [other]'s is the - * second). - * @return either [this] result's or [other]'s failure, if either are failing, or a successful - * result containing the value computed by [combine] - */ - private fun MathParsingResult.combineWith( - other: MathParsingResult, - combine: (I1, I2) -> O, - ): MathParsingResult { - return flatMap { result -> - other.map { otherResult -> - combine(result, otherResult) - } - } - } - - /** - * Performs the same operation as the other [combineWith] function, except with three - * [MathParsingResult]s, instead. - */ - private fun MathParsingResult.combineWith( - other1: MathParsingResult, - other2: MathParsingResult, - combine: (I1, I2, I3) -> O, - ): MathParsingResult { - return flatMap { result -> - other1.flatMap { otherResult1 -> - other2.map { otherResult2 -> - combine(result, otherResult1, otherResult2) - } - } - } - } - - private data class BinaryOperationRhs( - val operator: MathBinaryOperation.Operator, - val rhsResult: MathParsingResult, - val isImplicit: Boolean = false - ) { - fun computeBinaryOperationExpression( - lhsResult: MathParsingResult - ): MathParsingResult { - return lhsResult.combineWith(rhsResult) { lhs, rhs -> - MathExpression.newBuilder().apply { - parseStartIndex = lhs.parseStartIndex - parseEndIndex = rhs.parseEndIndex - binaryOperation = MathBinaryOperation.newBuilder().apply { - operator = this@BinaryOperationRhs.operator - leftOperand = lhs - rightOperand = rhs - isImplicit = this@BinaryOperationRhs.isImplicit - }.build() - }.build() - } - } - } - - private fun MathExpression.findFirstMultiRedundantGroup(): MathExpression? { - return when (expressionTypeCase) { - BINARY_OPERATION -> { - binaryOperation.leftOperand.findFirstMultiRedundantGroup() - ?: binaryOperation.rightOperand.findFirstMultiRedundantGroup() - } - UNARY_OPERATION -> unaryOperation.operand.findFirstMultiRedundantGroup() - FUNCTION_CALL -> functionCall.argument.findFirstMultiRedundantGroup() - GROUP -> group.takeIf { it.expressionTypeCase == GROUP } - ?: group.findFirstMultiRedundantGroup() - CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null - } - } - - private fun MathExpression.findNextRedundantGroup(): MathExpression? { - return when (expressionTypeCase) { - BINARY_OPERATION -> { - binaryOperation.leftOperand.findNextRedundantGroup() - ?: binaryOperation.rightOperand.findNextRedundantGroup() - } - UNARY_OPERATION -> unaryOperation.operand.findNextRedundantGroup() - FUNCTION_CALL -> functionCall.argument.findNextRedundantGroup() - GROUP -> group.takeIf { - it.expressionTypeCase in listOf(CONSTANT, VARIABLE) - } ?: group.findNextRedundantGroup() - CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null - } - } - - private fun MathExpression.findNextRedundantUnaryOperation(): MathExpression? { - return when (expressionTypeCase) { - BINARY_OPERATION -> { - binaryOperation.leftOperand.findNextRedundantUnaryOperation() - ?: binaryOperation.rightOperand.findNextRedundantUnaryOperation() - } - UNARY_OPERATION -> unaryOperation.operand.takeIf { - it.expressionTypeCase == UNARY_OPERATION - } ?: unaryOperation.operand.findNextRedundantUnaryOperation() - FUNCTION_CALL -> functionCall.argument.findNextRedundantUnaryOperation() - GROUP -> group.findNextRedundantUnaryOperation() - CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null - } - } - - private fun MathExpression.findNextExponentiationWithVariablePower(): MathExpression? { - return when (expressionTypeCase) { - BINARY_OPERATION -> { - takeIf { - binaryOperation.operator == EXPONENTIATE - && binaryOperation.rightOperand.isVariableExpression() - } ?: binaryOperation.leftOperand.findNextExponentiationWithVariablePower() - ?: binaryOperation.rightOperand.findNextExponentiationWithVariablePower() - } - UNARY_OPERATION -> unaryOperation.operand.findNextExponentiationWithVariablePower() - FUNCTION_CALL -> functionCall.argument.findNextExponentiationWithVariablePower() - GROUP -> group.findNextExponentiationWithVariablePower() - CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null - } - } - - private fun MathExpression.findNextExponentiationWithTooLargePower(): MathExpression? { - return when (expressionTypeCase) { - BINARY_OPERATION -> { - takeIf { - binaryOperation.operator == EXPONENTIATE - && binaryOperation.rightOperand.expressionTypeCase == CONSTANT - && binaryOperation.rightOperand.constant.toDouble() > 5.0 - } ?: binaryOperation.leftOperand.findNextExponentiationWithTooLargePower() - ?: binaryOperation.rightOperand.findNextExponentiationWithTooLargePower() - } - UNARY_OPERATION -> unaryOperation.operand.findNextExponentiationWithTooLargePower() - FUNCTION_CALL -> functionCall.argument.findNextExponentiationWithTooLargePower() - GROUP -> group.findNextExponentiationWithTooLargePower() - CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null - } - } - - private fun MathExpression.findNextNestedExponentiation(): MathExpression? { - return when (expressionTypeCase) { - BINARY_OPERATION -> { - takeIf { - binaryOperation.operator == EXPONENTIATE - && binaryOperation.rightOperand.containsExponentiation() - } ?: binaryOperation.leftOperand.findNextNestedExponentiation() - ?: binaryOperation.rightOperand.findNextNestedExponentiation() - } - UNARY_OPERATION -> unaryOperation.operand.findNextNestedExponentiation() - FUNCTION_CALL -> functionCall.argument.findNextNestedExponentiation() - GROUP -> group.findNextNestedExponentiation() - CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null - } - } - - private fun MathExpression.findNextDivisionByZero(): MathExpression? { - return when (expressionTypeCase) { - BINARY_OPERATION -> { - takeIf { - binaryOperation.operator == DIVIDE - && binaryOperation.rightOperand.expressionTypeCase == CONSTANT - && binaryOperation.rightOperand.constant - .toDouble().absoluteValue.approximatelyEquals(0.0) - } ?: binaryOperation.leftOperand.findNextDivisionByZero() - ?: binaryOperation.rightOperand.findNextDivisionByZero() - } - UNARY_OPERATION -> unaryOperation.operand.findNextDivisionByZero() - FUNCTION_CALL -> functionCall.argument.findNextDivisionByZero() - GROUP -> group.findNextDivisionByZero() - CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null - } - } - - private fun MathExpression.findAllDisallowedVariables(context: ParseContext): Set { - return if (context is AlgebraicExpressionContext) { - findAllDisallowedVariablesAux(context) - } else setOf() - } - - private fun MathExpression.findAllDisallowedVariablesAux( - context: AlgebraicExpressionContext - ): Set { - return when (expressionTypeCase) { - VARIABLE -> if (context.allowsVariable(variable)) setOf() else setOf(variable) - BINARY_OPERATION -> { - binaryOperation.leftOperand.findAllDisallowedVariablesAux(context) + - binaryOperation.rightOperand.findAllDisallowedVariablesAux(context) - } - UNARY_OPERATION -> unaryOperation.operand.findAllDisallowedVariablesAux(context) - FUNCTION_CALL -> functionCall.argument.findAllDisallowedVariablesAux(context) - GROUP -> group.findAllDisallowedVariablesAux(context) - CONSTANT, EXPRESSIONTYPE_NOT_SET, null -> setOf() - } - } - - private fun MathExpression.isVariableExpression(): Boolean { - return when (expressionTypeCase) { - VARIABLE -> true - BINARY_OPERATION -> { - binaryOperation.leftOperand.isVariableExpression() - || binaryOperation.rightOperand.isVariableExpression() - } - UNARY_OPERATION -> unaryOperation.operand.isVariableExpression() - FUNCTION_CALL -> functionCall.argument.isVariableExpression() - GROUP -> group.isVariableExpression() - CONSTANT, EXPRESSIONTYPE_NOT_SET, null -> false - } - } - - private fun MathExpression.containsExponentiation(): Boolean { - return when (expressionTypeCase) { - BINARY_OPERATION -> { - binaryOperation.operator == EXPONENTIATE - || binaryOperation.leftOperand.containsExponentiation() - || binaryOperation.rightOperand.containsExponentiation() - } - UNARY_OPERATION -> unaryOperation.operand.containsExponentiation() - FUNCTION_CALL -> functionCall.argument.containsExponentiation() - GROUP -> group.containsExponentiation() - CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> false - } - } - } -} diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 2b4874b56ab..88a20980d75 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -12,12 +12,13 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", - "//testing:assertion_helpers", "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:extensions", "//utility/src/main/java/org/oppia/android/util/math:parser", ], ) @@ -39,22 +40,3 @@ oppia_android_test( "//utility/src/main/java/org/oppia/android/util/math:tokenizer", ], ) - -oppia_android_test( - name = "NumericExpressionParserTest", - srcs = ["NumericExpressionParserTest.kt"], - custom_package = "org.oppia.android.util.math", - test_class = "org.oppia.android.util.math.NumericExpressionParserTest", - test_manifest = "//utility:test_manifest", - deps = [ - "//model/src/main/proto:math_java_proto_lite", - "//third_party:androidx_test_ext_junit", - "//third_party:com_google_truth_extensions_truth-liteproto-extension", - "//third_party:com_google_truth_truth", - "//third_party:junit_junit", - "//third_party:org_robolectric_robolectric", - "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:extensions", - "//utility/src/main/java/org/oppia/android/util/math:numeric_expression_parser", - ], -) diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index 9e9f29ee842..e24c93ef740 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -1,1154 +1,7737 @@ package org.oppia.android.util.math import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.BooleanSubject +import com.google.common.truth.DoubleSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.StringSubject +import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import com.google.common.truth.extensions.proto.LiteProtoSubject import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.MathBinaryOperation -import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD -import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE -import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE -import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY -import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT import org.oppia.android.app.model.MathExpression -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT import org.oppia.android.app.model.MathUnaryOperation -import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.Real -import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL -import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL -import org.oppia.android.testing.assertThrows -import org.oppia.android.util.math.MathExpressionParser.Companion.ParseResult -import org.oppia.android.util.math.MathExpressionParser.Companion.ParseResult.Failure -import org.oppia.android.util.math.MathExpressionParser.Companion.ParseResult.Success import org.robolectric.annotation.LooperMode +import kotlin.math.sqrt +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.PRODUCT +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.OppiaLanguage +import org.oppia.android.app.model.OppiaLanguage.ARABIC +import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.ENGLISH +import org.oppia.android.app.model.OppiaLanguage.HINDI +import org.oppia.android.app.model.OppiaLanguage.HINGLISH +import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED +import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED +import org.oppia.android.app.model.Polynomial +import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError +import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError +import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError +import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError +import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError +import org.oppia.android.util.math.MathParsingError.FunctionNameIncompleteError +import org.oppia.android.util.math.MathParsingError.HangingSquareRootError +import org.oppia.android.util.math.MathParsingError.InvalidFunctionInUseError +import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.NestedExponentsError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberAfterBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberBeforeBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NumberAfterVariableError +import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError +import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError +import org.oppia.android.util.math.MathParsingError.SubsequentBinaryOperatorsError +import org.oppia.android.util.math.MathParsingError.SubsequentUnaryOperatorsError +import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError +import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError +import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError +import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult /** Tests for [MathExpressionParser]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class MathExpressionParserTest { -// @Test -// fun testParse_emptyString_returnsFailure() { -// } - - // test various incorrect formatting errors including: - // - invalid variables - // - incorrect parentheses - // - consecutive binary operators - // - multiple expressions at the root - - // TODO: add support for implied multiplication (e.g. 'xy' should imply x*y). Ditto for coefficients. - // TOOD: test decimals, long variables, etc. - -// val result = MathExpressionParser.parseExpression("1") - // val result = MathExpressionParser.parseExpression("133 + 3.14 * x / (11 - 15) ^ 2 ^ 3") -// val result = MathExpressionParser.parseExpression("3 + 4 * 2 / (1 - 5) ^ 2 ^ 3") - // Test: 10/-1*-2 to verify unary precedence. - // Test unary at: beginning, after open paren, close paren (should be minus), and after operators - // Test multiple (2 & 3) unaries after each of the above - // Test invalid operator & operand cases, e.g. multiple operands or operators in wrong place - -// val result = MathExpressionParser.parseExpression("x^2*y^2 + 2") // works -// val result = MathExpressionParser.parseExpression("(x-1)^3") // works -// val result = MathExpressionParser.parseExpression("(x+1)/2") // fails -// val result = MathExpressionParser.parseExpression("x^2-3*x-10") // works -// val result = MathExpressionParser.parseExpression("x+2") // works -// val result = MathExpressionParser.parseExpression("4*(x+2)") // works -// val result = MathExpressionParser.parseExpression("(x^2-3*x-10)*(x+2)") // works -// val result = MathExpressionParser.parseExpression("(x^2-3*x-10)/(x+2)") // fails -// val polynomial = (result as Success).mathExpression.toPolynomial() - -// println("@@@@@ Result: ${(result as Success).mathExpression}}") -// println("@@@@@ Polynomial: $polynomial") -// println("@@@@@ Polynomial str: ${polynomial?.toAnswerString()}") - -// assertThat(result).isInstanceOf(Success::class.java) -// val expression = (result as Success).mathExpression -// assertThat(expression.expressionTypeCase).isEqualTo(MathExpression.ExpressionTypeCase.CONSTANT) -// assertThat(expression.constant.realTypeCase).isEqualTo(Real.RealTypeCase.RATIONAL) -// assertThat(expression.constant.rational).isEqualTo(createWholeNumberFraction(1)) + @Test + fun testErrorCases() { + val failure1 = expectFailureWhenParsingNumericExpression("73 2") + assertThat(failure1).isEqualTo(SpacesBetweenNumbersError) + + val failure2 = expectFailureWhenParsingNumericExpression("(73") + assertThat(failure2).isEqualTo(UnbalancedParenthesesError) + + val failure3 = expectFailureWhenParsingNumericExpression("73)") + assertThat(failure3).isEqualTo(UnbalancedParenthesesError) + + val failure4 = expectFailureWhenParsingNumericExpression("((73)") + assertThat(failure4).isEqualTo(UnbalancedParenthesesError) + + val failure5 = expectFailureWhenParsingNumericExpression("73 (") + assertThat(failure5).isEqualTo(UnbalancedParenthesesError) + + val failure6 = expectFailureWhenParsingNumericExpression("73 )") + assertThat(failure6).isEqualTo(UnbalancedParenthesesError) + + val failure7 = expectFailureWhenParsingNumericExpression("sqrt(73") + assertThat(failure7).isEqualTo(UnbalancedParenthesesError) + + // TODO: test properties on errors (& add better testing library for errors, or at least helpers). + val failure8 = expectFailureWhenParsingNumericExpression("(7 * 2 + 4)") + assertThat(failure8).isInstanceOf(SingleRedundantParenthesesError::class.java) + + val failure9 = expectFailureWhenParsingNumericExpression("((5 + 4))") + assertThat(failure9).isInstanceOf(MultipleRedundantParenthesesError::class.java) + + val failure13 = expectFailureWhenParsingNumericExpression("(((5 + 4)))") + assertThat(failure13).isInstanceOf(MultipleRedundantParenthesesError::class.java) + + val failure14 = expectFailureWhenParsingNumericExpression("1+((5 + 4))") + assertThat(failure14).isInstanceOf(MultipleRedundantParenthesesError::class.java) + + val failure15 = expectFailureWhenParsingNumericExpression("1+(7*((( 9 + 3) )))") + assertThat(failure15).isInstanceOf(MultipleRedundantParenthesesError::class.java) + assertThat((failure15 as MultipleRedundantParenthesesError).rawExpression) + .isEqualTo("(( 9 + 3) )") + + parseNumericExpressionWithAllErrors("1+(5+4)") + parseNumericExpressionWithAllErrors("(5+4)+1") + + val failure10 = expectFailureWhenParsingNumericExpression("(5) + 4") + assertThat(failure10).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + + val failure11 = expectFailureWhenParsingNumericExpression("5^(2)") + assertThat(failure11).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + assertThat((failure11 as RedundantParenthesesForIndividualTermsError).rawExpression) + .isEqualTo("2") + + val failure12 = expectFailureWhenParsingNumericExpression("sqrt((2))") + assertThat(failure12).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + + val failure16 = expectFailureWhenParsingNumericExpression("$2") + assertThat(failure16).isInstanceOf(UnnecessarySymbolsError::class.java) + assertThat((failure16 as UnnecessarySymbolsError).invalidSymbol).isEqualTo("$") + + val failure17 = expectFailureWhenParsingNumericExpression("5%") + assertThat(failure17).isInstanceOf(UnnecessarySymbolsError::class.java) + assertThat((failure17 as UnnecessarySymbolsError).invalidSymbol).isEqualTo("%") + + val failure18 = expectFailureWhenParsingAlgebraicExpression("x5") + assertThat(failure18).isInstanceOf(NumberAfterVariableError::class.java) + assertThat((failure18 as NumberAfterVariableError).number.integer).isEqualTo(5) + assertThat(failure18.variable).isEqualTo("x") + + val failure19 = expectFailureWhenParsingAlgebraicExpression("2+y 3.14*7") + assertThat(failure19).isInstanceOf(NumberAfterVariableError::class.java) + assertThat((failure19 as NumberAfterVariableError).number.irrational).isWithin(1e-5).of(3.14) + assertThat(failure19.variable).isEqualTo("y") + + // TODO: expand to multiple tests or use parametrized tests. + // RHS operators don't result in unary operations (which are valid in the grammar). + val rhsOperators = listOf("*", "×", "/", "÷", "^") + val lhsOperators = rhsOperators + listOf("+", "-", "−") + val operatorCombinations = lhsOperators.flatMap { op1 -> rhsOperators.map { op1 to it } } + for ((op1, op2) in operatorCombinations) { + val failure22 = expectFailureWhenParsingNumericExpression(expression = "1 $op1$op2 2") + assertThat(failure22).isInstanceOf(SubsequentBinaryOperatorsError::class.java) + assertThat((failure22 as SubsequentBinaryOperatorsError).operator1).isEqualTo(op1) + assertThat(failure22.operator2).isEqualTo(op2) + } + + val failure37 = expectFailureWhenParsingNumericExpression("++2") + assertThat(failure37).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure38 = expectFailureWhenParsingAlgebraicExpression("--x") + assertThat(failure38).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure39 = expectFailureWhenParsingAlgebraicExpression("-+x") + assertThat(failure39).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure40 = expectFailureWhenParsingNumericExpression("+-2") + assertThat(failure40).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + parseNumericExpressionWithAllErrors("2++3") // Will succeed since it's 2 + (+2). + val failure41 = expectFailureWhenParsingNumericExpression("2+++3") + assertThat(failure41).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure23 = expectFailureWhenParsingNumericExpression("/2") + assertThat(failure23).isInstanceOf(NoVariableOrNumberBeforeBinaryOperatorError::class.java) + assertThat((failure23 as NoVariableOrNumberBeforeBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.DIVIDE) + + val failure24 = expectFailureWhenParsingAlgebraicExpression("*x") + assertThat(failure24).isInstanceOf(NoVariableOrNumberBeforeBinaryOperatorError::class.java) + assertThat((failure24 as NoVariableOrNumberBeforeBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.MULTIPLY) + + val failure27 = expectFailureWhenParsingNumericExpression("2^") + assertThat(failure27).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure27 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.EXPONENTIATE) + + val failure25 = expectFailureWhenParsingNumericExpression("2/") + assertThat(failure25).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure25 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.DIVIDE) + + val failure26 = expectFailureWhenParsingAlgebraicExpression("x*") + assertThat(failure26).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure26 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.MULTIPLY) + + val failure28 = expectFailureWhenParsingAlgebraicExpression("x+") + assertThat(failure28).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure28 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.ADD) + + val failure29 = expectFailureWhenParsingAlgebraicExpression("x-") + assertThat(failure29).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure29 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.SUBTRACT) + + val failure42 = expectFailureWhenParsingAlgebraicExpression("2^x") + assertThat(failure42).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure43 = expectFailureWhenParsingAlgebraicExpression("2^(1+x)") + assertThat(failure43).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure44 = expectFailureWhenParsingAlgebraicExpression("2^3^x") + assertThat(failure44).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure45 = expectFailureWhenParsingAlgebraicExpression("2^sqrt(x)") + assertThat(failure45).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure46 = expectFailureWhenParsingNumericExpression("2^7") + assertThat(failure46).isInstanceOf(ExponentTooLargeError::class.java) + + val failure47 = expectFailureWhenParsingNumericExpression("2^30.12") + assertThat(failure47).isInstanceOf(ExponentTooLargeError::class.java) + + parseNumericExpressionWithAllErrors("2^3") + + val failure48 = expectFailureWhenParsingNumericExpression("2^3^2") + assertThat(failure48).isInstanceOf(NestedExponentsError::class.java) + + val failure49 = expectFailureWhenParsingAlgebraicExpression("x^2^5") + assertThat(failure49).isInstanceOf(NestedExponentsError::class.java) + + val failure20 = expectFailureWhenParsingNumericExpression("2√") + assertThat(failure20).isInstanceOf(HangingSquareRootError::class.java) + + val failure50 = expectFailureWhenParsingNumericExpression("2/0") + assertThat(failure50).isInstanceOf(TermDividedByZeroError::class.java) + + val failure51 = expectFailureWhenParsingAlgebraicExpression("x/0") + assertThat(failure51).isInstanceOf(TermDividedByZeroError::class.java) + + val failure52 = expectFailureWhenParsingNumericExpression("sqrt(2+7/0.0)") + assertThat(failure52).isInstanceOf(TermDividedByZeroError::class.java) + + val failure21 = expectFailureWhenParsingNumericExpression("x+y") + assertThat(failure21).isInstanceOf(VariableInNumericExpressionError::class.java) + + val failure53 = expectFailureWhenParsingAlgebraicExpression("x+y+a") + assertThat(failure53).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure53 as DisabledVariablesInUseError).variables).containsExactly("a") + + val failure54 = expectFailureWhenParsingAlgebraicExpression("apple") + assertThat(failure54).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure54 as DisabledVariablesInUseError).variables) + .containsExactly("a", "p", "l", "e") + + val failure55 = + expectFailureWhenParsingAlgebraicExpression("apple", allowedVariables = listOf("a", "p", "l")) + assertThat(failure55).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure55 as DisabledVariablesInUseError).variables).containsExactly("e") + + parseAlgebraicExpressionWithAllErrors("x+y+z") + + val failure56 = + expectFailureWhenParsingAlgebraicExpression("x+y+z", allowedVariables = listOf()) + assertThat(failure56).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure56 as DisabledVariablesInUseError).variables).containsExactly("x", "y", "z") + + val failure30 = expectFailureWhenParsingAlgebraicEquation("x==2") + assertThat(failure30).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + + val failure31 = expectFailureWhenParsingAlgebraicEquation("x=2=y") + assertThat(failure31).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + + val failure32 = expectFailureWhenParsingAlgebraicEquation("x=2=") + assertThat(failure32).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + + val failure33 = expectFailureWhenParsingAlgebraicEquation("x=") + assertThat(failure33).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + + val failure34 = expectFailureWhenParsingAlgebraicEquation("=x") + assertThat(failure34).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + + val failure35 = expectFailureWhenParsingAlgebraicEquation("=x") + assertThat(failure35).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + + // TODO: expand to multiple tests or use parametrized tests. + val prohibitedFunctionNames = + listOf( + "exp", "log", "log10", "ln", "sin", "cos", "tan", "cot", "csc", "sec", "atan", "asin", + "acos", "abs" + ) + for (functionName in prohibitedFunctionNames) { + val failure36 = expectFailureWhenParsingAlgebraicEquation("$functionName(0.5)") + assertThat(failure36).isInstanceOf(InvalidFunctionInUseError::class.java) + assertThat((failure36 as InvalidFunctionInUseError).functionName).isEqualTo(functionName) + } + + val failure57 = expectFailureWhenParsingAlgebraicExpression("sq") + assertThat(failure57).isInstanceOf(FunctionNameIncompleteError::class.java) + + val failure58 = expectFailureWhenParsingAlgebraicExpression("sqr") + assertThat(failure58).isInstanceOf(FunctionNameIncompleteError::class.java) + + // TODO: Other cases: sqrt(, sqrt(), sqrt 2, +2 + } + + @Test + fun testLotsOfCasesForNumericExpression() { + // TODO: split this up + // TODO: add log string generation for expressions. + expectFailureWhenParsingNumericExpression("") + + val expression1 = parseNumericExpressionWithoutOptionalErrors("1") + assertThat(expression1).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + assertThat(expression1).evaluatesToIntegerThat().isEqualTo(1) + + expectFailureWhenParsingNumericExpression("x") + + val expression2 = parseNumericExpressionWithoutOptionalErrors(" 2 ") + assertThat(expression2).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + assertThat(expression2).evaluatesToIntegerThat().isEqualTo(2) + + val expression3 = parseNumericExpressionWithoutOptionalErrors(" 2.5 ") + assertThat(expression3).hasStructureThatMatches { + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) + } + } + assertThat(expression3).evaluatesToIrrationalThat().isWithin(1e-5).of(2.5) + + expectFailureWhenParsingNumericExpression(" x ") + + expectFailureWhenParsingNumericExpression(" z x ") + + val expression4 = parseNumericExpressionWithoutOptionalErrors("2^3^2") + assertThat(expression4).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression4).evaluatesToIntegerThat().isEqualTo(512) + + val expression23 = parseNumericExpressionWithoutOptionalErrors("(2^3)^2") + assertThat(expression23).hasStructureThatMatches { + exponentiation { + leftOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + assertThat(expression23).evaluatesToIntegerThat().isEqualTo(64) + + val expression24 = parseNumericExpressionWithoutOptionalErrors("512/32/4") + assertThat(expression24).hasStructureThatMatches { + division { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(512) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(32) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + assertThat(expression24).evaluatesToIntegerThat().isEqualTo(4) + + val expression25 = parseNumericExpressionWithoutOptionalErrors("512/(32/4)") + assertThat(expression25).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(512) + } + } + rightOperand { + group { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(32) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + assertThat(expression25).evaluatesToIntegerThat().isEqualTo(64) + + val expression5 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)") + assertThat(expression5).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + assertThat(expression5).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0)) + + expectFailureWhenParsingNumericExpression("sqr(2)") + + expectFailureWhenParsingNumericExpression("xyz(2)") + + val expression6 = parseNumericExpressionWithoutOptionalErrors("732") + assertThat(expression6).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(732) + } + } + assertThat(expression6).evaluatesToIntegerThat().isEqualTo(732) + + // Verify order of operations between higher & lower precedent operators. + val expression32 = parseNumericExpressionWithoutOptionalErrors("3+4^7") + assertThat(expression32).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + } + } + assertThat(expression32).evaluatesToIntegerThat().isEqualTo(16387) + + val expression7 = parseNumericExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") + assertThat(expression7).hasStructureThatMatches { + // To better visualize the precedence & order of operations, see this grouped version: + // (((3*2)-3)+((((4^7)*8)/3)*2))+7. + addition { + leftOperand { + // ((3*2)-3)+((((4^7)*8)/3)*2) + addition { + leftOperand { + // (1*2)-3 + subtraction { + leftOperand { + // 3*2 + multiplication { + leftOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // (((4^7)*8)/3)*2 + multiplication { + leftOperand { + // ((4^7)*8)/3 + division { + leftOperand { + // (4^7)*8 + multiplication { + leftOperand { + // 4^7 + exponentiation { + leftOperand { + // 4 + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + rightOperand { + // 8 + constant { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + assertThat(expression7) + .evaluatesToRationalThat() + .evaluatesToRealThat() + .isWithin(1e-5) + .of(87391.333333333) + + expectFailureWhenParsingNumericExpression("x = √2 × 7 ÷ 4") + + val expression8 = parseNumericExpressionWithoutOptionalErrors("(1+2)(3+4)") + assertThat(expression8).hasStructureThatMatches { + multiplication { + leftOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + assertThat(expression8).evaluatesToIntegerThat().isEqualTo(21) + + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsingNumericExpression("(1+2)2") + + val expression10 = parseNumericExpressionWithoutOptionalErrors("2(1+2)") + assertThat(expression10).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + assertThat(expression10).evaluatesToIntegerThat().isEqualTo(6) + + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsingNumericExpression("sqrt(2)3") + + val expression12 = parseNumericExpressionWithoutOptionalErrors("3sqrt(2)") + assertThat(expression12).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression12).evaluatesToIrrationalThat().isWithin(1e-5).of(3.0 * sqrt(2.0)) + + expectFailureWhenParsingNumericExpression("xsqrt(2)") + + val expression13 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)*(1+2)*(3-2^5)") + assertThat(expression13).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + } + } + } + assertThat(expression13).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) + + val expression58 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)(1+2)(3-2^5)") + assertThat(expression58).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + } + } + } + assertThat(expression58).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) + + val expression14 = parseNumericExpressionWithoutOptionalErrors("((3))") + assertThat(expression14).hasStructureThatMatches { + group { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + assertThat(expression14).evaluatesToIntegerThat().isEqualTo(3) + + val expression15 = parseNumericExpressionWithoutOptionalErrors("++3") + assertThat(expression15).hasStructureThatMatches { + positive { + operand { + positive { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + assertThat(expression15).evaluatesToIntegerThat().isEqualTo(3) + + val expression16 = parseNumericExpressionWithoutOptionalErrors("--4") + assertThat(expression16).hasStructureThatMatches { + negation { + operand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression16).evaluatesToIntegerThat().isEqualTo(4) + + val expression17 = parseNumericExpressionWithoutOptionalErrors("1+-4") + assertThat(expression17).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression17).evaluatesToIntegerThat().isEqualTo(-3) + + val expression18 = parseNumericExpressionWithoutOptionalErrors("1++4") + assertThat(expression18).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + positive { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression18).evaluatesToIntegerThat().isEqualTo(5) + + val expression19 = parseNumericExpressionWithoutOptionalErrors("1--4") + assertThat(expression19).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression19).evaluatesToIntegerThat().isEqualTo(5) + + expectFailureWhenParsingNumericExpression("1-^-4") + + val expression20 = parseNumericExpressionWithoutOptionalErrors("√2 × 7 ÷ 4") + assertThat(expression20).hasStructureThatMatches { + division { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + assertThat(expression20).evaluatesToIrrationalThat().isWithin(1e-5).of((sqrt(2.0) * 7.0) / 4.0) + + expectFailureWhenParsingNumericExpression("1+2 &asdf") + + val expression21 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)sqrt(3)sqrt(4)") + // Note that this tree demonstrates left associativity. + assertThat(expression21).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression21) + .evaluatesToIrrationalThat() + .isWithin(1e-5) + .of(sqrt(2.0) * sqrt(3.0) * sqrt(4.0)) + + val expression22 = parseNumericExpressionWithoutOptionalErrors("(1+2)(3-7^2)(5+-17)") + assertThat(expression22).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + // 1+2 + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + // 3-7^2 + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + rightOperand { + // 5+-17 + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(17) + } + } + } + } + } + } + } + } + } + assertThat(expression22).evaluatesToIntegerThat().isEqualTo(1656) + + val expression26 = parseNumericExpressionWithoutOptionalErrors("3^-2") + assertThat(expression26).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression26).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(9) + } + + val expression27 = parseNumericExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") + assertThat(expression27).hasStructureThatMatches { + exponentiation { + leftOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + assertThat(expression27).evaluatesToIrrationalThat().isWithin(1e-5).of(0.78338103693) + + val expression28 = parseNumericExpressionWithoutOptionalErrors("1-3^sqrt(4)") + assertThat(expression28).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + assertThat(expression28).evaluatesToIntegerThat().isEqualTo(-8) + + // "Hard" order of operation problems loosely based on & other problems that can often stump + // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. + val expression29 = parseNumericExpressionWithoutOptionalErrors("3÷2*(3+4)") + assertThat(expression29).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + assertThat(expression29).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) + + val expression59 = parseNumericExpressionWithoutOptionalErrors("3÷2(3+4)") + assertThat(expression59).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + assertThat(expression59).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) + + // Numbers cannot have implicit multiplication unless they are in groups. + expectFailureWhenParsingNumericExpression("2 2") + + expectFailureWhenParsingNumericExpression("2 2^2") + + expectFailureWhenParsingNumericExpression("2^2 2") + + val expression31 = parseNumericExpressionWithoutOptionalErrors("(3)(4)(5)") + assertThat(expression31).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + assertThat(expression31).evaluatesToIntegerThat().isEqualTo(60) + + val expression33 = parseNumericExpressionWithoutOptionalErrors("2^(3)") + assertThat(expression33).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + assertThat(expression33).evaluatesToIntegerThat().isEqualTo(8) + + // Verify that implicit multiple has lower precedence than exponentiation. + val expression34 = parseNumericExpressionWithoutOptionalErrors("2^(3)(4)") + assertThat(expression34).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + assertThat(expression34).evaluatesToIntegerThat().isEqualTo(32) + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsingNumericExpression("2^(3)2^2") + + val expression35 = parseNumericExpressionWithoutOptionalErrors("2^(3)*2^2") + assertThat(expression35).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression35).evaluatesToIntegerThat().isEqualTo(32) + + // An exponentiation can be a right operand of an implicit mult if it's grouped. + val expression36 = parseNumericExpressionWithoutOptionalErrors("2^(3)(2^2)") + assertThat(expression36).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + assertThat(expression36).evaluatesToIntegerThat().isEqualTo(32) + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsingNumericExpression("2^3(4)2^3") + + val expression38 = parseNumericExpressionWithoutOptionalErrors("2^3(4)*2^3") + assertThat(expression38).hasStructureThatMatches { + // 2^3(4)*2^3 + multiplication { + leftOperand { + // 2^3(4) + multiplication { + leftOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 4 + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + rightOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + assertThat(expression38).evaluatesToIntegerThat().isEqualTo(256) + + expectFailureWhenParsingNumericExpression("2^2 2^2") + expectFailureWhenParsingNumericExpression("(3) 2^2") + expectFailureWhenParsingNumericExpression("sqrt(3) 2^2") + expectFailureWhenParsingNumericExpression("√2 2^2") + expectFailureWhenParsingNumericExpression("2^2 3") + + expectFailureWhenParsingNumericExpression("-2 3") + + val expression39 = parseNumericExpressionWithoutOptionalErrors("-(1+2)") + assertThat(expression39).hasStructureThatMatches { + negation { + operand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + assertThat(expression39).evaluatesToIntegerThat().isEqualTo(-3) + + // Should pass for algebra. + expectFailureWhenParsingNumericExpression("-2 x") + + val expression40 = parseNumericExpressionWithoutOptionalErrors("-2 (1+2)") + assertThat(expression40).hasStructureThatMatches { + // The negation happens last for parity with other common calculators. + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + assertThat(expression40).evaluatesToIntegerThat().isEqualTo(-6) + + val expression41 = parseNumericExpressionWithoutOptionalErrors("-2^3(4)") + assertThat(expression41).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + assertThat(expression41).evaluatesToIntegerThat().isEqualTo(-32) + + val expression43 = parseNumericExpressionWithoutOptionalErrors("√2^2(3)") + assertThat(expression43).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + assertThat(expression43).evaluatesToIrrationalThat().isWithin(1e-5).of(6.0) + + val expression60 = parseNumericExpressionWithoutOptionalErrors("√(2^2(3))") + assertThat(expression60).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + group { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + } + assertThat(expression60).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(12.0)) + + val expression42 = parseNumericExpressionWithoutOptionalErrors("-2*-2") + // Note that the following structure is not the same as (-2)*(-2) since unary negation has + // higher precedence than multiplication, so it's first & recurses to include the entire + // multiplication expression. + assertThat(expression42).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + assertThat(expression42).evaluatesToIntegerThat().isEqualTo(4) + + val expression44 = parseNumericExpressionWithoutOptionalErrors("2(2)") + assertThat(expression44).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + assertThat(expression44).evaluatesToIntegerThat().isEqualTo(4) + + val expression45 = parseNumericExpressionWithoutOptionalErrors("2sqrt(2)") + assertThat(expression45).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression45).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) + + val expression46 = parseNumericExpressionWithoutOptionalErrors("2√2") + assertThat(expression46).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression46).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) + + val expression47 = parseNumericExpressionWithoutOptionalErrors("(2)(2)") + assertThat(expression47).hasStructureThatMatches { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + assertThat(expression47).evaluatesToIntegerThat().isEqualTo(4) + + val expression48 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)(2)") + assertThat(expression48).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + assertThat(expression48).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) + + val expression49 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)sqrt(2)") + assertThat(expression49).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression49).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) + + val expression50 = parseNumericExpressionWithoutOptionalErrors("√2√2") + assertThat(expression50).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression50).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) + + val expression51 = parseNumericExpressionWithoutOptionalErrors("(2)(2)(2)") + assertThat(expression51).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + assertThat(expression51).evaluatesToIntegerThat().isEqualTo(8) + + val expression52 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)sqrt(2)sqrt(2)") + assertThat(expression52).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + val sqrt2 = sqrt(2.0) + assertThat(expression52).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) + + val expression53 = parseNumericExpressionWithoutOptionalErrors("√2√2√2") + assertThat(expression53).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression53).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) + + // Should fail for algebra. + expectFailureWhenParsingNumericExpression("x7") + + // Should pass for algebra. + expectFailureWhenParsingNumericExpression("2x^2") + + val expression54 = parseNumericExpressionWithoutOptionalErrors("2*2/-4+7*2") + assertThat(expression54).hasStructureThatMatches { + // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) + addition { + leftOperand { + // 2*2/-4 + division { + leftOperand { + // 2*2 + multiplication { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + // -4 + negation { + // 4 + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + rightOperand { + // 7*2 + multiplication { + leftOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression54).evaluatesToIntegerThat().isEqualTo(13) + + val expression55 = parseNumericExpressionWithoutOptionalErrors("3/(1-2)") + assertThat(expression55).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + assertThat(expression55).evaluatesToIntegerThat().isEqualTo(-3) + + val expression56 = parseNumericExpressionWithoutOptionalErrors("(3)/(1-2)") + assertThat(expression56).hasStructureThatMatches { + division { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + assertThat(expression56).evaluatesToIntegerThat().isEqualTo(-3) + + val expression57 = parseNumericExpressionWithoutOptionalErrors("3/((1-2))") + assertThat(expression57).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + group { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + assertThat(expression57).evaluatesToIntegerThat().isEqualTo(-3) + + // TODO: add others, including tests for malformed expressions throughout the parser & + // tokenizer. + } + + @Test + fun testLotsOfCasesForAlgebraicExpression() { + // TODO: split this up + // TODO: add log string generation for expressions. + expectFailureWhenParsingAlgebraicExpression("") + + val expression1 = parseAlgebraicExpressionWithoutOptionalErrors("1") + assertThat(expression1).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + assertThat(expression1).evaluatesToIntegerThat().isEqualTo(1) + + val expression61 = parseAlgebraicExpressionWithoutOptionalErrors("x") + assertThat(expression61).hasStructureThatMatches { + variable { + withNameThat().isEqualTo("x") + } + } + + val expression2 = parseAlgebraicExpressionWithoutOptionalErrors(" 2 ") + assertThat(expression2).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + assertThat(expression2).evaluatesToIntegerThat().isEqualTo(2) + + val expression3 = parseAlgebraicExpressionWithoutOptionalErrors(" 2.5 ") + assertThat(expression3).hasStructureThatMatches { + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) + } + } + assertThat(expression3).evaluatesToIrrationalThat().isWithin(1e-5).of(2.5) + + val expression62 = parseAlgebraicExpressionWithoutOptionalErrors(" y ") + assertThat(expression62).hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + + val expression63 = parseAlgebraicExpressionWithoutOptionalErrors(" z x ") + assertThat(expression63).hasStructureThatMatches { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("z") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + + val expression4 = parseAlgebraicExpressionWithoutOptionalErrors("2^3^2") + assertThat(expression4).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression4).evaluatesToIntegerThat().isEqualTo(512) + + val expression23 = parseAlgebraicExpressionWithoutOptionalErrors("(2^3)^2") + assertThat(expression23).hasStructureThatMatches { + exponentiation { + leftOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + assertThat(expression23).evaluatesToIntegerThat().isEqualTo(64) + + val expression24 = parseAlgebraicExpressionWithoutOptionalErrors("512/32/4") + assertThat(expression24).hasStructureThatMatches { + division { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(512) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(32) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + assertThat(expression24).evaluatesToIntegerThat().isEqualTo(4) + + val expression25 = parseAlgebraicExpressionWithoutOptionalErrors("512/(32/4)") + assertThat(expression25).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(512) + } + } + rightOperand { + group { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(32) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + assertThat(expression25).evaluatesToIntegerThat().isEqualTo(64) + + val expression5 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)") + assertThat(expression5).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + assertThat(expression5).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0)) + + expectFailureWhenParsingAlgebraicExpression("sqr(2)") + + val expression64 = parseAlgebraicExpressionWithoutOptionalErrors("xyz(2)") + assertThat(expression64).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("y") + } + } + } + } + rightOperand { + variable { + withNameThat().isEqualTo("z") + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression6 = parseAlgebraicExpressionWithoutOptionalErrors("732") + assertThat(expression6).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(732) + } + } + assertThat(expression6).evaluatesToIntegerThat().isEqualTo(732) + + expectFailureWhenParsingAlgebraicExpression("73 2") + + // Verify order of operations between higher & lower precedent operators. + val expression32 = parseAlgebraicExpressionWithoutOptionalErrors("3+4^7") + assertThat(expression32).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + } + } + assertThat(expression32).evaluatesToIntegerThat().isEqualTo(16387) + + val expression7 = parseAlgebraicExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") + assertThat(expression7).hasStructureThatMatches { + // To better visualize the precedence & order of operations, see this grouped version: + // (((3*2)-3)+((((4^7)*8)/3)*2))+7. + addition { + leftOperand { + // ((3*2)-3)+((((4^7)*8)/3)*2) + addition { + leftOperand { + // (1*2)-3 + subtraction { + leftOperand { + // 3*2 + multiplication { + leftOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // (((4^7)*8)/3)*2 + multiplication { + leftOperand { + // ((4^7)*8)/3 + division { + leftOperand { + // (4^7)*8 + multiplication { + leftOperand { + // 4^7 + exponentiation { + leftOperand { + // 4 + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + rightOperand { + // 8 + constant { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + assertThat(expression7) + .evaluatesToRationalThat() + .evaluatesToRealThat() + .isWithin(1e-5) + .of(87391.333333333) + + expectFailureWhenParsingAlgebraicExpression("x = √2 × 7 ÷ 4") + + val expression8 = parseAlgebraicExpressionWithoutOptionalErrors("(1+2)(3+4)") + assertThat(expression8).hasStructureThatMatches { + multiplication { + leftOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + assertThat(expression8).evaluatesToIntegerThat().isEqualTo(21) + + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsingAlgebraicExpression("(1+2)2") + + val expression10 = parseAlgebraicExpressionWithoutOptionalErrors("2(1+2)") + assertThat(expression10).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + assertThat(expression10).evaluatesToIntegerThat().isEqualTo(6) + + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsingAlgebraicExpression("sqrt(2)3") + + val expression12 = parseAlgebraicExpressionWithoutOptionalErrors("3sqrt(2)") + assertThat(expression12).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression12).evaluatesToIrrationalThat().isWithin(1e-5).of(3.0 * sqrt(2.0)) + + val expression65 = parseAlgebraicExpressionWithoutOptionalErrors("xsqrt(2)") + assertThat(expression65).hasStructureThatMatches { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression13 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)*(1+2)*(3-2^5)") + assertThat(expression13).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + } + } + } + assertThat(expression13).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) + + val expression58 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)(1+2)(3-2^5)") + assertThat(expression58).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + } + } + } + assertThat(expression58).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) + + val expression14 = parseAlgebraicExpressionWithoutOptionalErrors("((3))") + assertThat(expression14).hasStructureThatMatches { + group { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + assertThat(expression14).evaluatesToIntegerThat().isEqualTo(3) + + val expression15 = parseAlgebraicExpressionWithoutOptionalErrors("++3") + assertThat(expression15).hasStructureThatMatches { + positive { + operand { + positive { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + assertThat(expression15).evaluatesToIntegerThat().isEqualTo(3) + + val expression16 = parseAlgebraicExpressionWithoutOptionalErrors("--4") + assertThat(expression16).hasStructureThatMatches { + negation { + operand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression16).evaluatesToIntegerThat().isEqualTo(4) + + val expression17 = parseAlgebraicExpressionWithoutOptionalErrors("1+-4") + assertThat(expression17).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression17).evaluatesToIntegerThat().isEqualTo(-3) + + val expression18 = parseAlgebraicExpressionWithoutOptionalErrors("1++4") + assertThat(expression18).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + positive { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression18).evaluatesToIntegerThat().isEqualTo(5) + + val expression19 = parseAlgebraicExpressionWithoutOptionalErrors("1--4") + assertThat(expression19).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression19).evaluatesToIntegerThat().isEqualTo(5) + + expectFailureWhenParsingAlgebraicExpression("1-^-4") + + val expression20 = parseAlgebraicExpressionWithoutOptionalErrors("√2 × 7 ÷ 4") + assertThat(expression20).hasStructureThatMatches { + division { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + assertThat(expression20).evaluatesToIrrationalThat().isWithin(1e-5).of((sqrt(2.0) * 7.0) / 4.0) + + expectFailureWhenParsingAlgebraicExpression("1+2 &asdf") + + val expression21 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)sqrt(3)sqrt(4)") + // Note that this tree demonstrates left associativity. + assertThat(expression21).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + assertThat(expression21) + .evaluatesToIrrationalThat() + .isWithin(1e-5) + .of(sqrt(2.0) * sqrt(3.0) * sqrt(4.0)) + + val expression22 = parseAlgebraicExpressionWithoutOptionalErrors("(1+2)(3-7^2)(5+-17)") + assertThat(expression22).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + // 1+2 + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + // 3-7^2 + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + rightOperand { + // 5+-17 + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(17) + } + } + } + } + } + } + } + } + } + assertThat(expression22).evaluatesToIntegerThat().isEqualTo(1656) + + val expression26 = parseAlgebraicExpressionWithoutOptionalErrors("3^-2") + assertThat(expression26).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression26).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(9) + } + + val expression27 = parseAlgebraicExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") + assertThat(expression27).hasStructureThatMatches { + exponentiation { + leftOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + assertThat(expression27).evaluatesToIrrationalThat().isWithin(1e-5).of(0.78338103693) + + val expression28 = parseAlgebraicExpressionWithoutOptionalErrors("1-3^sqrt(4)") + assertThat(expression28).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + assertThat(expression28).evaluatesToIntegerThat().isEqualTo(-8) + + // "Hard" order of operation problems loosely based on & other problems that can often stump + // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. + val expression29 = parseAlgebraicExpressionWithoutOptionalErrors("3÷2*(3+4)") + assertThat(expression29).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + assertThat(expression29).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) + + val expression59 = parseAlgebraicExpressionWithoutOptionalErrors("3÷2(3+4)") + assertThat(expression59).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + assertThat(expression59).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) + + // Numbers cannot have implicit multiplication unless they are in groups. + expectFailureWhenParsingAlgebraicExpression("2 2") + + expectFailureWhenParsingAlgebraicExpression("2 2^2") + + expectFailureWhenParsingAlgebraicExpression("2^2 2") + + val expression31 = parseAlgebraicExpressionWithoutOptionalErrors("(3)(4)(5)") + assertThat(expression31).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + assertThat(expression31).evaluatesToIntegerThat().isEqualTo(60) + + val expression33 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)") + assertThat(expression33).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + assertThat(expression33).evaluatesToIntegerThat().isEqualTo(8) + + // Verify that implicit multiple has lower precedence than exponentiation. + val expression34 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(4)") + assertThat(expression34).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + assertThat(expression34).evaluatesToIntegerThat().isEqualTo(32) + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsingAlgebraicExpression("2^(3)2^2") + + val expression35 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)*2^2") + assertThat(expression35).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression35).evaluatesToIntegerThat().isEqualTo(32) + + // An exponentiation can be a right operand of an implicit mult if it's grouped. + val expression36 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(2^2)") + assertThat(expression36).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + assertThat(expression36).evaluatesToIntegerThat().isEqualTo(32) + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsingAlgebraicExpression("2^3(4)2^3") + + val expression38 = parseAlgebraicExpressionWithoutOptionalErrors("2^3(4)*2^3") + assertThat(expression38).hasStructureThatMatches { + // 2^3(4)*2^3 + multiplication { + leftOperand { + // 2^3(4) + multiplication { + leftOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 4 + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + rightOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + assertThat(expression38).evaluatesToIntegerThat().isEqualTo(256) + + expectFailureWhenParsingAlgebraicExpression("2^2 2^2") + expectFailureWhenParsingAlgebraicExpression("(3) 2^2") + expectFailureWhenParsingAlgebraicExpression("sqrt(3) 2^2") + expectFailureWhenParsingAlgebraicExpression("√2 2^2") + expectFailureWhenParsingAlgebraicExpression("2^2 3") + + expectFailureWhenParsingAlgebraicExpression("-2 3") + + val expression39 = parseAlgebraicExpressionWithoutOptionalErrors("-(1+2)") + assertThat(expression39).hasStructureThatMatches { + negation { + operand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + assertThat(expression39).evaluatesToIntegerThat().isEqualTo(-3) + + // Should pass for algebra. + val expression66 = parseAlgebraicExpressionWithoutOptionalErrors("-2 x") + assertThat(expression66).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + } + } + + val expression40 = parseAlgebraicExpressionWithoutOptionalErrors("-2 (1+2)") + assertThat(expression40).hasStructureThatMatches { + // The negation happens last for parity with other common calculators. + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + assertThat(expression40).evaluatesToIntegerThat().isEqualTo(-6) + + val expression41 = parseAlgebraicExpressionWithoutOptionalErrors("-2^3(4)") + assertThat(expression41).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + assertThat(expression41).evaluatesToIntegerThat().isEqualTo(-32) + + val expression43 = parseAlgebraicExpressionWithoutOptionalErrors("√2^2(3)") + assertThat(expression43).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + assertThat(expression43).evaluatesToIrrationalThat().isWithin(1e-5).of(6.0) + + val expression60 = parseAlgebraicExpressionWithoutOptionalErrors("√(2^2(3))") + assertThat(expression60).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + group { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + } + assertThat(expression60).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(12.0)) + + val expression42 = parseAlgebraicExpressionWithoutOptionalErrors("-2*-2") + // Note that the following structure is not the same as (-2)*(-2) since unary negation has + // higher precedence than multiplication, so it's first & recurses to include the entire + // multiplication expression. + assertThat(expression42).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + assertThat(expression42).evaluatesToIntegerThat().isEqualTo(4) + + val expression44 = parseAlgebraicExpressionWithoutOptionalErrors("2(2)") + assertThat(expression44).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + assertThat(expression44).evaluatesToIntegerThat().isEqualTo(4) + + val expression45 = parseAlgebraicExpressionWithoutOptionalErrors("2sqrt(2)") + assertThat(expression45).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression45).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) + + val expression46 = parseAlgebraicExpressionWithoutOptionalErrors("2√2") + assertThat(expression46).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression46).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) + + val expression47 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)") + assertThat(expression47).hasStructureThatMatches { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + assertThat(expression47).evaluatesToIntegerThat().isEqualTo(4) + + val expression48 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)(2)") + assertThat(expression48).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + assertThat(expression48).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) + + val expression49 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)sqrt(2)") + assertThat(expression49).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression49).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) + + val expression50 = parseAlgebraicExpressionWithoutOptionalErrors("√2√2") + assertThat(expression50).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression50).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) + + val expression51 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)(2)") + assertThat(expression51).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + assertThat(expression51).evaluatesToIntegerThat().isEqualTo(8) + + val expression52 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)sqrt(2)sqrt(2)") + assertThat(expression52).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + val sqrt2 = sqrt(2.0) + assertThat(expression52).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) + + val expression53 = parseAlgebraicExpressionWithoutOptionalErrors("√2√2√2") + assertThat(expression53).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression53).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) + + // Should fail for algebra. + expectFailureWhenParsingAlgebraicExpression("x7") + + // Should pass for algebra. + val expression67 = parseAlgebraicExpressionWithoutOptionalErrors("2x^2y^-3") + assertThat(expression67).hasStructureThatMatches { + // 2x^2y^-3 -> (2*(x^2))*(y^(-3)) + multiplication { + // 2x^2 + leftOperand { + multiplication { + // 2 + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + // x^2 + rightOperand { + exponentiation { + // x + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + // 2 + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + // y^-3 + rightOperand { + exponentiation { + // y + leftOperand { + variable { + withNameThat().isEqualTo("y") + } + } + // -3 + rightOperand { + negation { + // 3 + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + } + + val expression54 = parseAlgebraicExpressionWithoutOptionalErrors("2*2/-4+7*2") + assertThat(expression54).hasStructureThatMatches { + // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) + addition { + leftOperand { + // 2*2/-4 + division { + leftOperand { + // 2*2 + multiplication { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + // -4 + negation { + // 4 + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + rightOperand { + // 7*2 + multiplication { + leftOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + assertThat(expression54).evaluatesToIntegerThat().isEqualTo(13) + + val expression55 = parseAlgebraicExpressionWithoutOptionalErrors("3/(1-2)") + assertThat(expression55).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + assertThat(expression55).evaluatesToIntegerThat().isEqualTo(-3) + + val expression56 = parseAlgebraicExpressionWithoutOptionalErrors("(3)/(1-2)") + assertThat(expression56).hasStructureThatMatches { + division { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + assertThat(expression56).evaluatesToIntegerThat().isEqualTo(-3) + + val expression57 = parseAlgebraicExpressionWithoutOptionalErrors("3/((1-2))") + assertThat(expression57).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + group { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + assertThat(expression57).evaluatesToIntegerThat().isEqualTo(-3) + + // TODO: add others, including tests for malformed expressions throughout the parser & + // tokenizer. + } + + @Test + fun testLotsOfCasesForAlgebraicEquation() { + expectFailureWhenParsingAlgebraicEquation(" x =") + expectFailureWhenParsingAlgebraicEquation(" = y") + + val equation1 = parseAlgebraicEquationWithAllErrors("x = 1") + assertThat(equation1).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("x") + } + } + assertThat(equation1).hasRightHandSideThat().evaluatesToIntegerThat().isEqualTo(1) + + val equation2 = + parseAlgebraicEquationWithAllErrors("y = mx + b", allowedVariables = listOf("x", "y", "b", "m")) + assertThat(equation2).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + assertThat(equation2).hasRightHandSideThat().hasStructureThatMatches { + addition { + leftOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("m") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + rightOperand { + variable { + withNameThat().isEqualTo("b") + } + } + } + } + + val equation3 = parseAlgebraicEquationWithAllErrors("y = (x+1)^2") + assertThat(equation3).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + assertThat(equation3).hasRightHandSideThat().hasStructureThatMatches { + exponentiation { + leftOperand { + group { + addition { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + val equation4 = parseAlgebraicEquationWithAllErrors("y = (x+1)(x-1)") + assertThat(equation4).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + assertThat(equation4).hasRightHandSideThat().hasStructureThatMatches { + multiplication { + leftOperand { + group { + addition { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + } + } + + expectFailureWhenParsingAlgebraicEquation("y = (x+1)(x-1) 2") + expectFailureWhenParsingAlgebraicEquation("y 2 = (x+1)(x-1)") + + val equation5 = + parseAlgebraicEquationWithAllErrors("a*x^2 + b*x + c = 0", allowedVariables = listOf("x", "a", "b", "c")) + assertThat(equation5).hasLeftHandSideThat().hasStructureThatMatches { + addition { + leftOperand { + addition { + leftOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("a") + } + } + rightOperand { + exponentiation { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("b") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + } + } + rightOperand { + variable { + withNameThat().isEqualTo("c") + } + } + } + } + assertThat(equation5).hasRightHandSideThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(0) + } + } + } + + @Test + fun testLatex() { + // TODO: split up & move to separate test suites. Finish test cases. + + val exp1 = parseNumericExpressionWithAllErrors("1") + assertThat(exp1).convertsToLatexStringThat().isEqualTo("1") + + val exp2 = parseNumericExpressionWithAllErrors("1+2") + assertThat(exp2).convertsToLatexStringThat().isEqualTo("1 + 2") + + val exp3 = parseNumericExpressionWithAllErrors("1*2") + assertThat(exp3).convertsToLatexStringThat().isEqualTo("1 \\times 2") + + val exp4 = parseNumericExpressionWithAllErrors("1/2") + assertThat(exp4).convertsToLatexStringThat().isEqualTo("1 \\div 2") + + val exp5 = parseNumericExpressionWithAllErrors("1/2") + assertThat(exp5).convertsWithFractionsToLatexStringThat().isEqualTo("\\frac{1}{2}") + + val exp10 = parseNumericExpressionWithAllErrors("√2") + assertThat(exp10).convertsToLatexStringThat().isEqualTo("\\sqrt{2}") + + val exp11 = parseNumericExpressionWithAllErrors("√(1/2)") + assertThat(exp11).convertsToLatexStringThat().isEqualTo("\\sqrt{(1 \\div 2)}") + + val exp6 = parseAlgebraicExpressionWithAllErrors("x+y") + assertThat(exp6).convertsToLatexStringThat().isEqualTo("x + y") + + val exp7 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1/y)") + assertThat(exp7).convertsToLatexStringThat().isEqualTo("x ^ {(1 \\div y)}") + + val exp8 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1/y)") + assertThat(exp8).convertsWithFractionsToLatexStringThat().isEqualTo("x ^ {(\\frac{1}{y})}") + + val exp9 = parseAlgebraicExpressionWithoutOptionalErrors("x^y^z") + assertThat(exp9).convertsWithFractionsToLatexStringThat().isEqualTo("x ^ {y ^ {z}}") + + val eq1 = + parseAlgebraicEquationWithAllErrors( + "7a^2+b^2+c^2=0", allowedVariables = listOf("a", "b", "c") + ) + assertThat(eq1).convertsToLatexStringThat().isEqualTo("7a ^ {2} + b ^ {2} + c ^ {2} = 0") + + val eq2 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") + assertThat(eq2).convertsToLatexStringThat().isEqualTo("\\sqrt{1 + x} \\div x = 1") + + val eq3 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") + assertThat(eq3) + .convertsWithFractionsToLatexStringThat() + .isEqualTo("\\frac{\\sqrt{1 + x}}{x} = 1") + } + + @Test + fun testHumanReadableString() { + // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). + + val exp1 = parseNumericExpressionWithAllErrors("1") + assertThat(exp1).forHumanReadable(ARABIC).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(HINDI).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(HINGLISH).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(PORTUGUESE).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + + val exp2 = parseAlgebraicExpressionWithAllErrors("x") + assertThat(exp2).forHumanReadable(ARABIC).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(HINDI).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(HINGLISH).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(PORTUGUESE).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + + val eq1 = parseAlgebraicEquationWithAllErrors("x=1") + assertThat(eq1).forHumanReadable(ARABIC).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(HINDI).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(HINGLISH).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(PORTUGUESE).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + + // specific cases (from rules & other cases): + val exp3 = parseNumericExpressionWithAllErrors("1") + assertThat(exp3).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + assertThat(exp3).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp49 = parseNumericExpressionWithAllErrors("-1") + assertThat(exp49).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative 1") + + val exp50 = parseNumericExpressionWithAllErrors("+1") + assertThat(exp50).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive 1") + + val exp4 = parseNumericExpressionWithoutOptionalErrors("((1))") + assertThat(exp4).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp5 = parseNumericExpressionWithAllErrors("1+2") + assertThat(exp5).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus 2") + + val exp6 = parseNumericExpressionWithAllErrors("1-2") + assertThat(exp6).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus 2") + + val exp7 = parseNumericExpressionWithAllErrors("1*2") + assertThat(exp7).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times 2") + + val exp8 = parseNumericExpressionWithAllErrors("1/2") + assertThat(exp8).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by 2") + + val exp9 = parseNumericExpressionWithAllErrors("1+(1-2)") + assertThat(exp9) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("1 plus open parenthesis 1 minus 2 close parenthesis") + + val exp10 = parseNumericExpressionWithAllErrors("2^3") + assertThat(exp10) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("2 raised to the power of 3") + + val exp11 = parseNumericExpressionWithAllErrors("2^(1+2)") + assertThat(exp11) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("2 raised to the power of open parenthesis 1 plus 2 close parenthesis") + + val exp12 = parseNumericExpressionWithAllErrors("100000*2") + assertThat(exp12).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") + + val exp13 = parseNumericExpressionWithAllErrors("sqrt(2)") + assertThat(exp13).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + + val exp14 = parseNumericExpressionWithAllErrors("√2") + assertThat(exp14).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + + val exp15 = parseNumericExpressionWithAllErrors("sqrt(1+2)") + assertThat(exp15) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root 1 plus 2 end square root") + + val singularOrdinalNames = mapOf( + 1 to "oneth", + 2 to "half", + 3 to "third", + 4 to "fourth", + 5 to "fifth", + 6 to "sixth", + 7 to "seventh", + 8 to "eighth", + 9 to "ninth", + 10 to "tenth", + ) + val pluralOrdinalNames = mapOf( + 1 to "oneths", + 2 to "halves", + 3 to "thirds", + 4 to "fourths", + 5 to "fifths", + 6 to "sixths", + 7 to "sevenths", + 8 to "eighths", + 9 to "ninths", + 10 to "tenths", + ) + for (denominatorToCheck in 1..10) { + for (numeratorToCheck in 0..denominatorToCheck) { + val exp16 = parseNumericExpressionWithAllErrors("$numeratorToCheck/$denominatorToCheck") + + val ordinalName = + if (numeratorToCheck == 1) { + singularOrdinalNames.getValue(denominatorToCheck) + } else pluralOrdinalNames.getValue(denominatorToCheck) + assertThat(exp16) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("$numeratorToCheck $ordinalName") + } + } + + val exp17 = parseNumericExpressionWithAllErrors("-1/3") + assertThat(exp17) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("negative 1 third") + + val exp18 = parseNumericExpressionWithAllErrors("-2/3") + assertThat(exp18) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("negative 2 thirds") + + val exp19 = parseNumericExpressionWithAllErrors("10/11") + assertThat(exp19) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("10 over 11") + + val exp20 = parseNumericExpressionWithAllErrors("121/7986") + assertThat(exp20) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("121 over 7,986") + + val exp21 = parseNumericExpressionWithAllErrors("8/7") + assertThat(exp21) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("8 over 7") + + val exp22 = parseNumericExpressionWithAllErrors("-10/-30") + assertThat(exp22) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("negative the fraction with numerator 10 and denominator negative 30") + + val exp23 = parseAlgebraicExpressionWithAllErrors("1") + assertThat(exp23).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp24 = parseAlgebraicExpressionWithoutOptionalErrors("((1))") + assertThat(exp24).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp25 = parseAlgebraicExpressionWithAllErrors("x") + assertThat(exp25).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") + + val exp26 = parseAlgebraicExpressionWithoutOptionalErrors("((x))") + assertThat(exp26).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") + + val exp51 = parseAlgebraicExpressionWithAllErrors("-x") + assertThat(exp51).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative x") + + val exp52 = parseAlgebraicExpressionWithAllErrors("+x") + assertThat(exp52).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive x") + + val exp27 = parseAlgebraicExpressionWithAllErrors("1+x") + assertThat(exp27).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus x") + + val exp28 = parseAlgebraicExpressionWithAllErrors("1-x") + assertThat(exp28).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus x") + + val exp29 = parseAlgebraicExpressionWithAllErrors("1*x") + assertThat(exp29).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times x") + + val exp30 = parseAlgebraicExpressionWithAllErrors("1/x") + assertThat(exp30).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by x") + + val exp31 = parseAlgebraicExpressionWithAllErrors("1/x") + assertThat(exp31) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("the fraction with numerator 1 and denominator x") + + val exp32 = parseAlgebraicExpressionWithAllErrors("1+(1-x)") + assertThat(exp32) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("1 plus open parenthesis 1 minus x close parenthesis") + + val exp33 = parseAlgebraicExpressionWithAllErrors("2x") + assertThat(exp33).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x") + + val exp34 = parseAlgebraicExpressionWithAllErrors("xy") + assertThat(exp34).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x times y") + + val exp35 = parseAlgebraicExpressionWithAllErrors("z") + assertThat(exp35).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("zed") + + val exp36 = parseAlgebraicExpressionWithAllErrors("2xz") + assertThat(exp36).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x times zed") + + val exp37 = parseAlgebraicExpressionWithAllErrors("x^2") + assertThat(exp37) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x raised to the power of 2") + + val exp38 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1+x)") + assertThat(exp38) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x raised to the power of open parenthesis 1 plus x close parenthesis") + + val exp39 = parseAlgebraicExpressionWithAllErrors("100000*2") + assertThat(exp39).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") + + val exp40 = parseAlgebraicExpressionWithAllErrors("sqrt(2)") + assertThat(exp40).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + + val exp41 = parseAlgebraicExpressionWithAllErrors("sqrt(x)") + assertThat(exp41).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") + + val exp42 = parseAlgebraicExpressionWithAllErrors("√2") + assertThat(exp42).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + + val exp43 = parseAlgebraicExpressionWithAllErrors("√x") + assertThat(exp43).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") + + val exp44 = parseAlgebraicExpressionWithAllErrors("sqrt(1+2)") + assertThat(exp44) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root 1 plus 2 end square root") + + val exp45 = parseAlgebraicExpressionWithAllErrors("sqrt(1+x)") + assertThat(exp45) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root 1 plus x end square root") + + val exp46 = parseAlgebraicExpressionWithAllErrors("√(1+x)") + assertThat(exp46) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root open parenthesis 1 plus x close parenthesis end square root") + + for (denominatorToCheck in 1..10) { + for (numeratorToCheck in 0..denominatorToCheck) { + val exp16 = parseAlgebraicExpressionWithAllErrors("$numeratorToCheck/$denominatorToCheck") + + val ordinalName = + if (numeratorToCheck == 1) { + singularOrdinalNames.getValue(denominatorToCheck) + } else pluralOrdinalNames.getValue(denominatorToCheck) + assertThat(exp16) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("$numeratorToCheck $ordinalName") + } + } + + val exp47 = parseAlgebraicExpressionWithAllErrors("1") + assertThat(exp47).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp48 = parseAlgebraicExpressionWithAllErrors("x(5-y)") + assertThat(exp48) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x times open parenthesis 5 minus y close parenthesis") + + val eq2 = parseAlgebraicEquationWithAllErrors("x=1/y") + assertThat(eq2) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x equals 1 divided by y") + + val eq3 = parseAlgebraicEquationWithAllErrors("x=1/2") + assertThat(eq3) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x equals 1 divided by 2") + + val eq4 = parseAlgebraicEquationWithAllErrors("x=1/y") + assertThat(eq4) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("x equals the fraction with numerator 1 and denominator y") + + val eq5 = parseAlgebraicEquationWithAllErrors("x=1/2") + assertThat(eq5) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("x equals 1 half") + + // Tests from examples in the PRD + val eq6 = parseAlgebraicEquationWithAllErrors("3x^2+4y=62") + assertThat(eq6) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("3 x raised to the power of 2 plus 4 y equals 62") + + val exp53 = parseAlgebraicExpressionWithAllErrors("(x+6)/(x-4)") + assertThat(exp53) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo( + "the fraction with numerator open parenthesis x plus 6 close parenthesis and denominator" + + " open parenthesis x minus 4 close parenthesis" + ) + + val exp54 = parseAlgebraicExpressionWithoutOptionalErrors("4*(x)^(2)+20x") + assertThat(exp54) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("4 times x raised to the power of 2 plus 20 x") + + val exp55 = parseAlgebraicExpressionWithAllErrors("3+x-5") + assertThat(exp55).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("3 plus x minus 5") + + val exp56 = parseAlgebraicExpressionWithAllErrors("Z+A-Z", allowedVariables = listOf("A", "Z")) + assertThat(exp56).forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("Zed plus A minus Zed") + + val exp57 = + parseAlgebraicExpressionWithAllErrors("6C-5A-1", allowedVariables = listOf("A", "C")) + assertThat(exp57) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("6 C minus 5 A minus 1") + + val exp58 = parseAlgebraicExpressionWithAllErrors("5*Z-w", allowedVariables = listOf("Z", "w")) + assertThat(exp58) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("5 times Zed minus w") + + val exp59 = + parseAlgebraicExpressionWithAllErrors("L*S-3S+L", allowedVariables = listOf("L", "S")) + assertThat(exp59) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("L times S minus 3 S plus L") + + val exp60 = parseAlgebraicExpressionWithAllErrors("2*(2+6+3+4)") + assertThat(exp60) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("2 times open parenthesis 2 plus 6 plus 3 plus 4 close parenthesis") + + val exp61 = parseAlgebraicExpressionWithAllErrors("sqrt(64)") + assertThat(exp61) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("square root of 64") + + val exp62 = parseAlgebraicExpressionWithAllErrors("√(a+b)", allowedVariables = listOf("a", "b")) + assertThat(exp62) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root open parenthesis a plus b close parenthesis end square root") + + val exp63 = parseAlgebraicExpressionWithAllErrors("3*10^-5") + assertThat(exp63) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("3 times 10 raised to the power of negative 5") + + val exp64 = + parseAlgebraicExpressionWithoutOptionalErrors( + "((x+2y)+5*(a-2b)+z)", allowedVariables = listOf("x", "y", "a", "b", "z") + ) + assertThat(exp64) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo( + "open parenthesis open parenthesis x plus 2 y close parenthesis plus 5 times open" + + " parenthesis a minus 2 b close parenthesis plus zed close parenthesis" + ) + } + + @Test + fun testToComparableOperation() { + // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). + + val exp1 = parseNumericExpressionWithAllErrors("1") + assertThat(exp1.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + + val exp2 = parseNumericExpressionWithAllErrors("-1") + assertThat(exp2.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + + val exp3 = parseNumericExpressionWithAllErrors("1+3+4") + assertThat(exp3.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp4 = parseNumericExpressionWithAllErrors("-1-2-3") + assertThat(exp4.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp5 = parseNumericExpressionWithAllErrors("1+2-3") + assertThat(exp5.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp6 = parseNumericExpressionWithAllErrors("2*3*4") + assertThat(exp6.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp7 = parseNumericExpressionWithAllErrors("1-2*3") + assertThat(exp7.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + + val exp8 = parseNumericExpressionWithAllErrors("2*3-4") + assertThat(exp8.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp9 = parseNumericExpressionWithAllErrors("1+2*3-4+8*7*6-9") + assertThat(exp9.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(3) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(9) + } + } + } + } + + val exp10 = parseNumericExpressionWithAllErrors("2/3/4") + assertThat(exp10.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp11 = parseNumericExpressionWithoutOptionalErrors("2^3^4") + assertThat(exp11.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + val exp12 = parseNumericExpressionWithAllErrors("1+2/3+3") + assertThat(exp12.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp13 = parseNumericExpressionWithAllErrors("1+(2/3)+3") + assertThat(exp13.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp14 = parseNumericExpressionWithAllErrors("1+2^3+3") + assertThat(exp14.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp15 = parseNumericExpressionWithAllErrors("1+(2^3)+3") + assertThat(exp15.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp16 = parseNumericExpressionWithAllErrors("2*3/4*7") + assertThat(exp16.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp17 = parseNumericExpressionWithAllErrors("2*(3/4)*7") + assertThat(exp17.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp18 = parseNumericExpressionWithAllErrors("-3*sqrt(2)") + assertThat(exp18.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + squareRootWithArgument { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp19 = parseNumericExpressionWithAllErrors("1+(2+(3+(4+5)))") + assertThat(exp19.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + + val exp20 = parseNumericExpressionWithAllErrors("2*(3*(4*(5*6)))") + assertThat(exp20.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + } + } + + val exp21 = parseAlgebraicExpressionWithAllErrors("x") + assertThat(exp21.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + + val exp22 = parseAlgebraicExpressionWithAllErrors("-x") + assertThat(exp22.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + + val exp23 = parseAlgebraicExpressionWithAllErrors("1+x+y") + assertThat(exp23.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp24 = parseAlgebraicExpressionWithAllErrors("-1-x-y") + assertThat(exp24.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp25 = parseAlgebraicExpressionWithAllErrors("1+x-y") + assertThat(exp25.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp26 = parseAlgebraicExpressionWithAllErrors("2xy") + assertThat(exp26.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp27 = parseAlgebraicExpressionWithAllErrors("1-xy") + assertThat(exp27.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + + val exp28 = parseAlgebraicExpressionWithAllErrors("xy-4") + assertThat(exp28.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp29 = parseAlgebraicExpressionWithAllErrors("1+xy-4+yz-9") + assertThat(exp29.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(3) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(9) + } + } + } + } + + val exp30 = parseAlgebraicExpressionWithAllErrors("2/x/y") + assertThat(exp30.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp31 = parseAlgebraicExpressionWithoutOptionalErrors("x^3^4") + assertThat(exp31.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + val exp32 = parseAlgebraicExpressionWithAllErrors("1+x/y+z") + assertThat(exp32.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + val exp33 = parseAlgebraicExpressionWithAllErrors("1+(x/y)+z") + assertThat(exp33.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + val exp34 = parseAlgebraicExpressionWithAllErrors("1+x^3+y") + assertThat(exp34.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp35 = parseAlgebraicExpressionWithAllErrors("1+(x^3)+y") + assertThat(exp35.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp36 = parseAlgebraicExpressionWithAllErrors("2*x/y*z") + assertThat(exp36.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp37 = parseAlgebraicExpressionWithAllErrors("2*(x/y)*z") + assertThat(exp37.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp38 = parseAlgebraicExpressionWithAllErrors("-2*sqrt(x)") + assertThat(exp38.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + squareRootWithArgument { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + val exp39 = parseAlgebraicExpressionWithAllErrors("1+(x+(3+(z+y)))") + assertThat(exp39.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + val exp40 = parseAlgebraicExpressionWithAllErrors("2*(x*(4*(zy)))") + assertThat(exp40.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + // Equality tests: + val list1 = createComparableOperationListFromNumericExpression("(1+2)+3") + val list2 = createComparableOperationListFromNumericExpression("1+(2+3)") + assertThat(list1).isEqualTo(list2) + + val list3 = createComparableOperationListFromNumericExpression("1+2+3") + val list4 = createComparableOperationListFromNumericExpression("3+2+1") + assertThat(list3).isEqualTo(list4) + + val list5 = createComparableOperationListFromNumericExpression("1-2-3") + val list6 = createComparableOperationListFromNumericExpression("-3 + -2 + 1") + assertThat(list5).isEqualTo(list6) + + val list7 = createComparableOperationListFromNumericExpression("1-2-3") + val list8 = createComparableOperationListFromNumericExpression("-3-2+1") + assertThat(list7).isEqualTo(list8) + + val list9 = createComparableOperationListFromNumericExpression("1-2-3") + val list10 = createComparableOperationListFromNumericExpression("-3-2+1") + assertThat(list9).isEqualTo(list10) + + val list11 = createComparableOperationListFromNumericExpression("1-2-3") + val list12 = createComparableOperationListFromNumericExpression("3-2-1") + assertThat(list11).isNotEqualTo(list12) + + val list13 = createComparableOperationListFromNumericExpression("2*3*4") + val list14 = createComparableOperationListFromNumericExpression("4*3*2") + assertThat(list13).isEqualTo(list14) + + val list15 = createComparableOperationListFromNumericExpression("2*(3/4)") + val list16 = createComparableOperationListFromNumericExpression("3/4*2") + assertThat(list15).isEqualTo(list16) + + val list17 = createComparableOperationListFromNumericExpression("2*3/4") + val list18 = createComparableOperationListFromNumericExpression("3/4*2") + assertThat(list17).isEqualTo(list18) + + val list45 = createComparableOperationListFromNumericExpression("2*3/4") + val list46 = createComparableOperationListFromNumericExpression("2*3*4") + assertThat(list45).isNotEqualTo(list46) + + val list19 = createComparableOperationListFromNumericExpression("2*3/4") + val list20 = createComparableOperationListFromNumericExpression("2*4/3") + assertThat(list19).isNotEqualTo(list20) + + val list21 = createComparableOperationListFromNumericExpression("2*3/4*7") + val list22 = createComparableOperationListFromNumericExpression("3/4*7*2") + assertThat(list21).isEqualTo(list22) + + val list23 = createComparableOperationListFromNumericExpression("2*3/4*7") + val list24 = createComparableOperationListFromNumericExpression("7*(3*2/4)") + assertThat(list23).isEqualTo(list24) + + val list25 = createComparableOperationListFromNumericExpression("2*3/4*7") + val list26 = createComparableOperationListFromNumericExpression("7*3*2/4") + assertThat(list25).isEqualTo(list26) + + val list27 = createComparableOperationListFromNumericExpression("-2*3") + val list28 = createComparableOperationListFromNumericExpression("3*-2") + assertThat(list27).isEqualTo(list28) + + val list29 = createComparableOperationListFromNumericExpression("2^3") + val list30 = createComparableOperationListFromNumericExpression("3^2") + assertThat(list29).isNotEqualTo(list30) + + val list31 = createComparableOperationListFromNumericExpression("-(1+2)") + val list32 = createComparableOperationListFromNumericExpression("-1+2") + assertThat(list31).isNotEqualTo(list32) + + val list33 = createComparableOperationListFromNumericExpression("-(1+2)") + val list34 = createComparableOperationListFromNumericExpression("-1-2") + assertThat(list33).isNotEqualTo(list34) + + val list35 = createComparableOperationListFromAlgebraicExpression("x(x+1)") + val list36 = createComparableOperationListFromAlgebraicExpression("(1+x)x") + assertThat(list35).isEqualTo(list36) + + val list37 = createComparableOperationListFromAlgebraicExpression("x(x+1)") + val list38 = createComparableOperationListFromAlgebraicExpression("x^2+x") + assertThat(list37).isNotEqualTo(list38) - @Test - fun testParse_constantWholeNumber_parsesSuccessfully() { - val result = MathExpressionParser.parseExpression("1", allowedVariables = listOf()) + val list39 = createComparableOperationListFromAlgebraicExpression("x^2*sqrt(x)") + val list40 = createComparableOperationListFromAlgebraicExpression("x") + assertThat(list39).isNotEqualTo(list40) - assertThat(result).isInstanceOf(Success::class.java) - } + val list41 = createComparableOperationListFromAlgebraicExpression("xyz") + val list42 = createComparableOperationListFromAlgebraicExpression("zyx") + assertThat(list41).isEqualTo(list42) - @Test - fun testParse_constantWholeNumber_returnsExpressionWithFractionWholeNumber() { - val result = MathExpressionParser.parseExpression("1", allowedVariables = listOf()) + val list43 = createComparableOperationListFromAlgebraicExpression("1+xy-2") + val list44 = createComparableOperationListFromAlgebraicExpression("-2+1+yx") + assertThat(list43).isEqualTo(list44) - val rootExpression = result.getExpectedSuccessfulExpression() - assertThat(result).isInstanceOf(Success::class.java) - assertThat(rootExpression.constant.realTypeCase).isEqualTo(RATIONAL) - assertThat(rootExpression.constant.rational).isEqualTo(createWholeNumberFraction(1)) + // TODO: add tests for comparator/sorting & negation simplification? } @Test - fun testParse_constantDecimalNumber_returnsExpressionWithIrrationalNumber() { - val result = MathExpressionParser.parseExpression("3.14", allowedVariables = listOf()) + fun testPolynomials() { + // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). + + val poly1 = parseNumericExpressionWithAllErrors("1").toPolynomial() + assertThat(poly1).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly1).isConstantThat().isIntegerThat().isEqualTo(1) + + val poly13 = parseNumericExpressionWithAllErrors("1-1").toPolynomial() + assertThat(poly13).evaluatesToPlainTextThat().isEqualTo("0") + assertThat(poly13).isConstantThat().isIntegerThat().isEqualTo(0) + + val poly2 = parseNumericExpressionWithAllErrors("3 + 4 * 2 / (1 - 5) ^ 2").toPolynomial() + assertThat(poly2).evaluatesToPlainTextThat().isEqualTo("7/2") + assertThat(poly2).isConstantThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(3) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } - val rootExpression = result.getExpectedSuccessfulExpression() - assertThat(rootExpression.expressionTypeCase).isEqualTo(CONSTANT) - assertThat(rootExpression.constant.realTypeCase).isEqualTo(IRRATIONAL) - assertThat(rootExpression.constant.irrational).isWithin(1e-5).of(3.14) - } + val poly3 = parseAlgebraicExpressionWithAllErrors("133+3.14*x/(11-15)^2").toPolynomial() + assertThat(poly3).evaluatesToPlainTextThat().isEqualTo("0.19625x + 133") + assertThat(poly3).hasTermCountThat().isEqualTo(2) + assertThat(poly3).term(0).hasCoefficientThat().isIrrationalThat().isWithin(1e-5).of(0.19625) + assertThat(poly3).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly3).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly3).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly3).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(133) + assertThat(poly3).term(1).hasVariableCountThat().isEqualTo(0) + + val poly4 = parseAlgebraicExpressionWithAllErrors("x^2").toPolynomial() + assertThat(poly4).evaluatesToPlainTextThat().isEqualTo("x^2") + assertThat(poly4).hasTermCountThat().isEqualTo(1) + assertThat(poly4).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly4).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly4).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly4).term(0).variable(0).hasPowerThat().isEqualTo(2) + + val poly5 = parseAlgebraicExpressionWithAllErrors("xy+x").toPolynomial() + assertThat(poly5).evaluatesToPlainTextThat().isEqualTo("xy + x") + assertThat(poly5).hasTermCountThat().isEqualTo(2) + assertThat(poly5).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly5).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly5).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly5).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly5).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly5).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly5).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly5).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly5).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly5).term(1).variable(0).hasPowerThat().isEqualTo(1) + + val poly6 = parseAlgebraicExpressionWithAllErrors("2x").toPolynomial() + assertThat(poly6).evaluatesToPlainTextThat().isEqualTo("2x") + assertThat(poly6).hasTermCountThat().isEqualTo(1) + assertThat(poly6).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly6).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) + assertThat(poly6).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly6).term(0).variable(0).hasPowerThat().isEqualTo(1) + + val poly30 = parseAlgebraicExpressionWithAllErrors("x+2").toPolynomial() + assertThat(poly30).evaluatesToPlainTextThat().isEqualTo("x + 2") + assertThat(poly30).hasTermCountThat().isEqualTo(2) + assertThat(poly30).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly30).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(0) + } - @Test - fun testParse_variable_returnsExpressionWithVariable() { - val result = MathExpressionParser.parseExpression("x", allowedVariables = listOf("x")) + val poly29 = parseAlgebraicExpressionWithAllErrors("x^2-3*x-10").toPolynomial() + assertThat(poly29).evaluatesToPlainTextThat().isEqualTo("x^2 - 3x - 10") + assertThat(poly29).hasTermCountThat().isEqualTo(3) + assertThat(poly29).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly29).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly29).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-10) + hasVariableCountThat().isEqualTo(0) + } - val rootExpression = result.getExpectedSuccessfulExpression() - assertThat(rootExpression.expressionTypeCase).isEqualTo(VARIABLE) - assertThat(rootExpression.variable).isEqualTo("x") - } + val poly31 = parseAlgebraicExpressionWithAllErrors("4*(x+2)").toPolynomial() + assertThat(poly31).evaluatesToPlainTextThat().isEqualTo("4x + 8") + assertThat(poly31).hasTermCountThat().isEqualTo(2) + assertThat(poly31).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(4) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly31).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(8) + hasVariableCountThat().isEqualTo(0) + } - @Test - fun testParse_multipleShortVariables_returnsExpressionWithBothVariables() { - val result = MathExpressionParser.parseExpression("x+y", allowedVariables = listOf("x", "y")) - - val rootExpression = result.getExpectedSuccessfulExpression() - val binOp = rootExpression.getExpectedBinaryOperation() - val leftVar = binOp.leftOperand.getExpectedVariable() - val rightVar = binOp.rightOperand.getExpectedVariable() - assertThat(leftVar).isEqualTo("x") - assertThat(rightVar).isEqualTo("y") - } + val poly7 = parseAlgebraicExpressionWithAllErrors("2xy^2z^3").toPolynomial() + assertThat(poly7).evaluatesToPlainTextThat().isEqualTo("2xy^2z^3") + assertThat(poly7).hasTermCountThat().isEqualTo(1) + assertThat(poly7).term(0).hasVariableCountThat().isEqualTo(3) + assertThat(poly7).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) + assertThat(poly7).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly7).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly7).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly7).term(0).variable(1).hasPowerThat().isEqualTo(2) + assertThat(poly7).term(0).variable(2).hasNameThat().isEqualTo("z") + assertThat(poly7).term(0).variable(2).hasPowerThat().isEqualTo(3) + + // Show that 7+xy+yz-3-xz-yz+3xy-4 combines into 4xy-xz (the eliminated terms should be gone). + val poly8 = parseAlgebraicExpressionWithAllErrors("xy+yz-xz-yz+3xy").toPolynomial() + assertThat(poly8).evaluatesToPlainTextThat().isEqualTo("4xy - xz") + assertThat(poly8).hasTermCountThat().isEqualTo(2) + assertThat(poly8).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly8).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(4) + assertThat(poly8).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly8).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly8).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly8).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly8).term(1).hasVariableCountThat().isEqualTo(2) + assertThat(poly8).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(-1) + assertThat(poly8).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly8).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly8).term(1).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly8).term(1).variable(1).hasPowerThat().isEqualTo(1) + + // x+2x should become 3x since like terms are combined. + val poly9 = parseAlgebraicExpressionWithAllErrors("x+2x").toPolynomial() + assertThat(poly9).evaluatesToPlainTextThat().isEqualTo("3x") + assertThat(poly9).hasTermCountThat().isEqualTo(1) + assertThat(poly9).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly9).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(3) + assertThat(poly9).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly9).term(0).variable(0).hasPowerThat().isEqualTo(1) + + // xx^2 should become x^3 since like terms are combined. + val poly10 = parseAlgebraicExpressionWithAllErrors("xx^2").toPolynomial() + assertThat(poly10).evaluatesToPlainTextThat().isEqualTo("x^3") + assertThat(poly10).hasTermCountThat().isEqualTo(1) + assertThat(poly10).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly10).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly10).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly10).term(0).variable(0).hasPowerThat().isEqualTo(3) + + // No terms in this polynomial should be combined. + val poly11 = parseAlgebraicExpressionWithAllErrors("x^2+x+1").toPolynomial() + assertThat(poly11).evaluatesToPlainTextThat().isEqualTo("x^2 + x + 1") + assertThat(poly11).hasTermCountThat().isEqualTo(3) + assertThat(poly11).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly11).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly11).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly11).term(0).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly11).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly11).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly11).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly11).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly11).term(2).hasVariableCountThat().isEqualTo(0) + assertThat(poly11).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + + // No terms in this polynomial should be combined. + val poly12 = parseAlgebraicExpressionWithAllErrors("x^2 + x^2y").toPolynomial() + assertThat(poly12).evaluatesToPlainTextThat().isEqualTo("x^2y + x^2") + assertThat(poly12).hasTermCountThat().isEqualTo(2) + assertThat(poly12).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly12).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly12).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly12).term(0).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly12).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly12).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly12).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly12).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly12).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly12).term(1).variable(0).hasPowerThat().isEqualTo(2) + + // Ordering tests. Verify that ordering matches + // https://en.wikipedia.org/wiki/Polynomial#Definition (where multiple variables are sorted + // lexicographically). + + // The order of the terms in this polynomial should be reversed. + val poly14 = parseAlgebraicExpressionWithAllErrors("1+x+x^2+x^3").toPolynomial() + assertThat(poly14).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") + assertThat(poly14).hasTermCountThat().isEqualTo(4) + assertThat(poly14).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly14).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly14).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly14).term(0).variable(0).hasPowerThat().isEqualTo(3) + assertThat(poly14).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly14).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly14).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly14).term(1).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly14).term(2).hasVariableCountThat().isEqualTo(1) + assertThat(poly14).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly14).term(2).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly14).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly14).term(3).hasVariableCountThat().isEqualTo(0) + assertThat(poly14).term(3).hasCoefficientThat().isIntegerThat().isEqualTo(1) + + // The order of the terms in this polynomial should be preserved. + val poly15 = parseAlgebraicExpressionWithAllErrors("x^3+x^2+x+1").toPolynomial() + assertThat(poly15).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") + assertThat(poly15).hasTermCountThat().isEqualTo(4) + assertThat(poly15).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly15).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly15).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly15).term(0).variable(0).hasPowerThat().isEqualTo(3) + assertThat(poly15).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly15).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly15).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly15).term(1).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly15).term(2).hasVariableCountThat().isEqualTo(1) + assertThat(poly15).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly15).term(2).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly15).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly15).term(3).hasVariableCountThat().isEqualTo(0) + assertThat(poly15).term(3).hasCoefficientThat().isIntegerThat().isEqualTo(1) + + // The order of the terms in this polynomial should be reversed. + val poly16 = parseAlgebraicExpressionWithAllErrors("xy+xz+yz").toPolynomial() + assertThat(poly16).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") + assertThat(poly16).hasTermCountThat().isEqualTo(3) + assertThat(poly16).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly16).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly16).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly16).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly16).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(1).hasVariableCountThat().isEqualTo(2) + assertThat(poly16).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly16).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly16).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(1).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly16).term(1).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(2).hasVariableCountThat().isEqualTo(2) + assertThat(poly16).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly16).term(2).variable(0).hasNameThat().isEqualTo("y") + assertThat(poly16).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(2).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly16).term(2).variable(1).hasPowerThat().isEqualTo(1) + + // The order of the terms in this polynomial should be preserved. + val poly17 = parseAlgebraicExpressionWithAllErrors("yz+xz+xy").toPolynomial() + assertThat(poly17).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") + assertThat(poly17).hasTermCountThat().isEqualTo(3) + assertThat(poly17).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly17).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly17).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly17).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly17).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(1).hasVariableCountThat().isEqualTo(2) + assertThat(poly17).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly17).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly17).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(1).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly17).term(1).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(2).hasVariableCountThat().isEqualTo(2) + assertThat(poly17).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly17).term(2).variable(0).hasNameThat().isEqualTo("y") + assertThat(poly17).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(2).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly17).term(2).variable(1).hasPowerThat().isEqualTo(1) + + val poly18 = parseAlgebraicExpressionWithAllErrors("3+x+y+xy+x^2y+xy^2+x^2y^2").toPolynomial() + assertThat(poly18).evaluatesToPlainTextThat().isEqualTo("x^2y^2 + x^2y + xy^2 + xy + x + y + 3") + assertThat(poly18).hasTermCountThat().isEqualTo(7) + assertThat(poly18).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly18).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly18).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(4).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(5).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(6).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(0) + } - @Test - fun testParse_longVariable_returnsExpressionWithVariable() { - val result = - MathExpressionParser.parseExpression("1+lambda", allowedVariables = listOf("lambda")) - - val rootExpression = result.getExpectedSuccessfulExpression() - val binOp = rootExpression.getExpectedBinaryOperation() - val rightVar = binOp.rightOperand.getExpectedVariable() - assertThat(rightVar).isEqualTo("lambda") - } + // Ensure variables of coefficient and power of 0 are removed. + val poly22 = parseAlgebraicExpressionWithAllErrors("0x").toPolynomial() + assertThat(poly22).evaluatesToPlainTextThat().isEqualTo("0") + assertThat(poly22).hasTermCountThat().isEqualTo(1) + assertThat(poly22).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(0) + hasVariableCountThat().isEqualTo(0) + } - @Test - fun testParse_mixedLongAndShortVariable_returnsExpressionWithBothVariables() { - val result = - MathExpressionParser.parseExpression("lambda+y", allowedVariables = listOf("y", "lambda")) - - val rootExpression = result.getExpectedSuccessfulExpression() - val binOp = rootExpression.getExpectedBinaryOperation() - val leftVar = binOp.leftOperand.getExpectedVariable() - val rightVar = binOp.rightOperand.getExpectedVariable() - assertThat(leftVar).isEqualTo("lambda") - assertThat(rightVar).isEqualTo("y") - } + val poly23 = parseAlgebraicExpressionWithAllErrors("x-x").toPolynomial() + assertThat(poly23).evaluatesToPlainTextThat().isEqualTo("0") + assertThat(poly23).hasTermCountThat().isEqualTo(1) + assertThat(poly23).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(0) + hasVariableCountThat().isEqualTo(0) + } - @Test - fun testParse_negation_returnsUnaryExpressionNegatingVariable() { - val result = MathExpressionParser.parseExpression("-x", allowedVariables = listOf("x")) - - val rootExpression = result.getExpectedSuccessfulExpression() - val unaryOp = rootExpression.getExpectedUnaryOperation() - val variableOperand = unaryOp.operand.getExpectedVariable() - assertThat(unaryOp.operator).isEqualTo(NEGATE) - assertThat(variableOperand).isEqualTo("x") - } + val poly24 = parseAlgebraicExpressionWithAllErrors("x^0").toPolynomial() + assertThat(poly24).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly24).hasTermCountThat().isEqualTo(1) + assertThat(poly24).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } - @Test - fun testParse_negateExpression_returnsUnaryExpressionBeingNegated() { - val result = MathExpressionParser.parseExpression("-(x+2)", allowedVariables = listOf("x")) + val poly25 = parseAlgebraicExpressionWithAllErrors("x/x").toPolynomial() + assertThat(poly25).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly25).hasTermCountThat().isEqualTo(1) + assertThat(poly25).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } - val rootExpression = result.getExpectedSuccessfulExpression() - val unaryOp = rootExpression.getExpectedUnaryOperationWithOperator(NEGATE) - // The entire operation should be negated. - assertThat(unaryOp.operand.expressionTypeCase).isEqualTo(BINARY_OPERATION) - } + val poly26 = parseAlgebraicExpressionWithAllErrors("x^(2-2)").toPolynomial() + assertThat(poly26).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly26).hasTermCountThat().isEqualTo(1) + assertThat(poly26).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } - @Test - fun testParse_doubleNegate_cascadesInTree() { - val result = MathExpressionParser.parseExpression("--x", allowedVariables = listOf("x")) - - // Expected tree (left-to-right): negate(negate(x)) - val rootExpression = result.getExpectedSuccessfulExpression() - val outerOp = rootExpression.getExpectedUnaryOperationWithOperator(NEGATE) - val innerOp = outerOp.operand.getExpectedUnaryOperationWithOperator(NEGATE) - assertThat(innerOp.operand.expressionTypeCase).isEqualTo(VARIABLE) - } + val poly28 = parseAlgebraicExpressionWithAllErrors("(x+1)/2").toPolynomial() + assertThat(poly28).evaluatesToPlainTextThat().isEqualTo("(1/2)x + 1/2") + assertThat(poly28).hasTermCountThat().isEqualTo(2) + assertThat(poly28).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly28).term(1).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(0) + } - @Test - fun testParse_addVariableAndConstant_returnsBinaryExpression() { - val result = MathExpressionParser.parseExpression("x+2", allowedVariables = listOf("x")) - - val rootExpression = result.getExpectedSuccessfulExpression() - val binaryOp = rootExpression.getExpectedBinaryOperation() - val leftVariable = binaryOp.leftOperand.getExpectedVariable() - val rightConstant = binaryOp.rightOperand.getExpectedConstant() - assertThat(binaryOp.operator).isEqualTo(ADD) - assertThat(leftVariable).isEqualTo("x") - assertThat(rightConstant.realTypeCase).isEqualTo(RATIONAL) - assertThat(rightConstant.rational).isEqualTo(createWholeNumberFraction(2)) - } + // Ensure like terms are combined after polynomial multiplication. + val poly20 = parseAlgebraicExpressionWithAllErrors("(x-5)(x+2)").toPolynomial() + assertThat(poly20).evaluatesToPlainTextThat().isEqualTo("x^2 - 3x - 10") + assertThat(poly20).hasTermCountThat().isEqualTo(3) + assertThat(poly20).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly20).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly20).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-10) + hasVariableCountThat().isEqualTo(0) + } - @Test - fun testParse_addVariableAndIrrationalConstant_returnsBinaryExpression() { - val result = MathExpressionParser.parseExpression("x+2.718", allowedVariables = listOf("x")) - - val rootExpression = result.getExpectedSuccessfulExpression() - val binaryOp = rootExpression.getExpectedBinaryOperation() - val leftVariable = binaryOp.leftOperand.getExpectedVariable() - val rightConstant = binaryOp.rightOperand.getExpectedConstant() - assertThat(binaryOp.operator).isEqualTo(ADD) - assertThat(leftVariable).isEqualTo("x") - assertThat(rightConstant.realTypeCase).isEqualTo(IRRATIONAL) - assertThat(rightConstant.irrational).isWithin(1e-5).of(2.718) - } + val poly21 = parseAlgebraicExpressionWithAllErrors("(1+x)^3").toPolynomial() + assertThat(poly21).evaluatesToPlainTextThat().isEqualTo("x^3 + 3x^2 + 3x + 1") + assertThat(poly21).hasTermCountThat().isEqualTo(4) + assertThat(poly21).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + assertThat(poly21).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly21).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly21).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } - @Test - fun testParse_addConstantWithNegatedVariable_combinesUnaryAndBinaryOperations() { - val result = MathExpressionParser.parseExpression("2+-x", allowedVariables = listOf("x")) - - // Expected tree (left-to-right): add(2, negate(x)) - val rootExpression = result.getExpectedSuccessfulExpression() - val binaryOp = rootExpression.getExpectedBinaryOperationWithOperator(ADD) - val leftConstant = binaryOp.leftOperand.getExpectedRationalConstant() - val rightNegateOp = binaryOp.rightOperand.getExpectedUnaryOperationWithOperator(NEGATE) - val negatedVariable = rightNegateOp.operand.getExpectedVariable() - assertThat(leftConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(negatedVariable).isEqualTo("x") - } + val poly27 = parseAlgebraicExpressionWithAllErrors("x^2*y^2 + 2").toPolynomial() + assertThat(poly27).evaluatesToPlainTextThat().isEqualTo("x^2y^2 + 2") + assertThat(poly27).hasTermCountThat().isEqualTo(2) + assertThat(poly27).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly27).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(0) + } - @Test - fun testParse_subtractVariableAndConstant_returnsBinaryExpression() { - val result = MathExpressionParser.parseExpression("x-2", allowedVariables = listOf("x")) - - val rootExpression = result.getExpectedSuccessfulExpression() - val binaryOp = rootExpression.getExpectedBinaryOperation() - val leftVariable = binaryOp.leftOperand.getExpectedVariable() - val rightConstant = binaryOp.rightOperand.getExpectedConstant() - assertThat(binaryOp.operator).isEqualTo(SUBTRACT) - assertThat(leftVariable).isEqualTo("x") - assertThat(rightConstant.realTypeCase).isEqualTo(RATIONAL) - assertThat(rightConstant.rational).isEqualTo(createWholeNumberFraction(2)) - } + val poly32 = parseAlgebraicExpressionWithAllErrors("(x^2-3*x-10)*(x+2)").toPolynomial() + assertThat(poly32).evaluatesToPlainTextThat().isEqualTo("x^3 - x^2 - 16x - 20") + assertThat(poly32).hasTermCountThat().isEqualTo(4) + assertThat(poly32).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + assertThat(poly32).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly32).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-16) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly32).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-20) + hasVariableCountThat().isEqualTo(0) + } - @Test - fun testParse_multiplyVariableAndConstant_returnsBinaryExpression() { - val result = MathExpressionParser.parseExpression("x × 2", allowedVariables = listOf("x")) - - val rootExpression = result.getExpectedSuccessfulExpression() - val binaryOp = rootExpression.getExpectedBinaryOperation() - val leftVariable = binaryOp.leftOperand.getExpectedVariable() - val rightConstant = binaryOp.rightOperand.getExpectedConstant() - assertThat(binaryOp.operator).isEqualTo(MULTIPLY) - assertThat(leftVariable).isEqualTo("x") - assertThat(rightConstant.realTypeCase).isEqualTo(RATIONAL) - assertThat(rightConstant.rational).isEqualTo(createWholeNumberFraction(2)) - } + val poly33 = parseAlgebraicExpressionWithAllErrors("(x-y)^3").toPolynomial() + assertThat(poly33).evaluatesToPlainTextThat().isEqualTo("x^3 - 3x^2y + 3xy^2 - y^3") + assertThat(poly33).hasTermCountThat().isEqualTo(4) + assertThat(poly33).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + assertThat(poly33).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly33).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly33).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(3) + } + } - @Test - fun testParse_divideVariableAndConstant_returnsBinaryExpression() { - val result = MathExpressionParser.parseExpression("x ÷ 2", allowedVariables = listOf("x")) - - val rootExpression = result.getExpectedSuccessfulExpression() - val binaryOp = rootExpression.getExpectedBinaryOperation() - val leftVariable = binaryOp.leftOperand.getExpectedVariable() - val rightConstant = binaryOp.rightOperand.getExpectedConstant() - assertThat(binaryOp.operator).isEqualTo(DIVIDE) - assertThat(leftVariable).isEqualTo("x") - assertThat(rightConstant.realTypeCase).isEqualTo(RATIONAL) - assertThat(rightConstant.rational).isEqualTo(createWholeNumberFraction(2)) - } + // Ensure polynomial division works. + val poly19 = parseAlgebraicExpressionWithAllErrors("(x^2-3*x-10)/(x+2)").toPolynomial() + assertThat(poly19).evaluatesToPlainTextThat().isEqualTo("x - 5") + assertThat(poly19).hasTermCountThat().isEqualTo(2) + assertThat(poly19).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly19).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-5) + hasVariableCountThat().isEqualTo(0) + } - @Test - fun testParse_variableRaisedByConstant_returnsBinaryExpression() { - val result = MathExpressionParser.parseExpression("x^2", allowedVariables = listOf("x")) - - val rootExpression = result.getExpectedSuccessfulExpression() - val binaryOp = rootExpression.getExpectedBinaryOperation() - val leftVariable = binaryOp.leftOperand.getExpectedVariable() - val rightConstant = binaryOp.rightOperand.getExpectedConstant() - assertThat(binaryOp.operator).isEqualTo(EXPONENTIATE) - assertThat(leftVariable).isEqualTo("x") - assertThat(rightConstant.realTypeCase).isEqualTo(RATIONAL) - assertThat(rightConstant.rational).isEqualTo(createWholeNumberFraction(2)) - } + val poly35 = parseAlgebraicExpressionWithAllErrors("(xy-5y)/y").toPolynomial() + assertThat(poly35).evaluatesToPlainTextThat().isEqualTo("x - 5") + assertThat(poly35).hasTermCountThat().isEqualTo(2) + assertThat(poly35).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly35).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-5) + hasVariableCountThat().isEqualTo(0) + } - @Test - fun testParse_binaryAddition_withRedundantParentheses_returnsSameBinaryExpression() { - // Test that extra parentheses don't change the result. - val result = MathExpressionParser.parseExpression("(x+2)", allowedVariables = listOf("x")) - - val rootExpression = result.getExpectedSuccessfulExpression() - val binaryOp = rootExpression.getExpectedBinaryOperation() - val leftVariable = binaryOp.leftOperand.getExpectedVariable() - val rightConstant = binaryOp.rightOperand.getExpectedConstant() - assertThat(binaryOp.operator).isEqualTo(ADD) - assertThat(leftVariable).isEqualTo("x") - assertThat(rightConstant.realTypeCase).isEqualTo(RATIONAL) - assertThat(rightConstant.rational).isEqualTo(createWholeNumberFraction(2)) - } + val poly36 = parseAlgebraicExpressionWithAllErrors("(x^2-2xy+y^2)/(x-y)").toPolynomial() + assertThat(poly36).evaluatesToPlainTextThat().isEqualTo("x - y") + assertThat(poly36).hasTermCountThat().isEqualTo(2) + assertThat(poly36).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly36).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } - @Test - fun testParse_twoAdditions_haveLeftToRightAssociativity() { - val result = MathExpressionParser.parseExpression("1+3+2", allowedVariables = listOf()) - - // Left-to-right associativity means the first encountered addition is done first, and the - // second is done last (which means it's at the root). Expect the following tree: - // + - // + 2 - // 1 3 - val rootExpression = result.getExpectedSuccessfulExpression() - val rootAddOp = rootExpression.getExpectedBinaryOperationWithOperator(ADD) - val rootLeftAddOp = rootAddOp.leftOperand.getExpectedBinaryOperationWithOperator(ADD) - val rootRightConstant = rootAddOp.rightOperand.getExpectedRationalConstant() - val innerOpLeftConstant = rootLeftAddOp.leftOperand.getExpectedRationalConstant() - val innerOpRightConstant = rootLeftAddOp.rightOperand.getExpectedRationalConstant() - assertThat(rootRightConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(1)) - assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(3)) - } + // Example from https://www.kristakingmath.com/blog/predator-prey-systems-ghtcp-5e2r4-427ab. + val poly37 = parseAlgebraicExpressionWithAllErrors("(x^3-y^3)/(x-y)").toPolynomial() + assertThat(poly37).evaluatesToPlainTextThat().isEqualTo("x^2 + xy + y^2") + assertThat(poly37).hasTermCountThat().isEqualTo(3) + assertThat(poly37).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly37).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly37).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } - @Test - fun testParse_additionThenSubtraction_haveLeftToRightAssociativity() { - val result = MathExpressionParser.parseExpression("1+3-2", allowedVariables = listOf()) - - // Left-to-right associativity means the first encountered addition is done first, and the - // second is done last (which means it's at the root). Expect the following tree: - // - - // + 2 - // 1 3 - val rootExpression = result.getExpectedSuccessfulExpression() - val rootSubOp = rootExpression.getExpectedBinaryOperationWithOperator(SUBTRACT) - val rootLeftAddOp = rootSubOp.leftOperand.getExpectedBinaryOperationWithOperator(ADD) - val rootRightConstant = rootSubOp.rightOperand.getExpectedRationalConstant() - val innerOpLeftConstant = rootLeftAddOp.leftOperand.getExpectedRationalConstant() - val innerOpRightConstant = rootLeftAddOp.rightOperand.getExpectedRationalConstant() - assertThat(rootRightConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(1)) - assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(3)) - } + // Multi-variable & more complex division. + val poly34 = + parseAlgebraicExpressionWithAllErrors("(x^3-3x^2y+3xy^2-y^3)/(x-y)^2").toPolynomial() + assertThat(poly34).evaluatesToPlainTextThat().isEqualTo("x - y") + assertThat(poly34).hasTermCountThat().isEqualTo(2) + assertThat(poly34).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly34).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } - @Test - fun testParse_subtractionThenAddition_haveLeftToRightAssociativity() { - val result = MathExpressionParser.parseExpression("1-3+2", allowedVariables = listOf()) - - // Left-to-right associativity means the first encountered addition is done first, and the - // second is done last (which means it's at the root). Expect the following tree: - // + - // - 2 - // 1 3 - val rootExpression = result.getExpectedSuccessfulExpression() - val rootAddOp = rootExpression.getExpectedBinaryOperationWithOperator(ADD) - val rootLeftSubOp = rootAddOp.leftOperand.getExpectedBinaryOperationWithOperator(SUBTRACT) - val rootRightConstant = rootAddOp.rightOperand.getExpectedRationalConstant() - val innerOpLeftConstant = rootLeftSubOp.leftOperand.getExpectedRationalConstant() - val innerOpRightConstant = rootLeftSubOp.rightOperand.getExpectedRationalConstant() - assertThat(rootRightConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(1)) - assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(3)) - } + val poly38 = parseNumericExpressionWithAllErrors("2^-4").toPolynomial() + assertThat(poly38).evaluatesToPlainTextThat().isEqualTo("1/16") + assertThat(poly38).hasTermCountThat().isEqualTo(1) + assertThat(poly38).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(16) + } + hasVariableCountThat().isEqualTo(0) + } - @Test - fun testParse_twoMultiplies_haveLeftToRightAssociativity() { - val result = MathExpressionParser.parseExpression("1*3*2", allowedVariables = listOf()) - - // Left-to-right associativity means the first encountered binary op is done first, and the - // second is done last (which means it's at the root). Expect the following tree: - // * - // * 2 - // 1 3 - val rootExpression = result.getExpectedSuccessfulExpression() - val rootMulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val rootLeftMulOp = rootMulOp.leftOperand.getExpectedBinaryOperationWithOperator(MULTIPLY) - val rootRightConstant = rootMulOp.rightOperand.getExpectedRationalConstant() - val innerOpLeftConstant = rootLeftMulOp.leftOperand.getExpectedRationalConstant() - val innerOpRightConstant = rootLeftMulOp.rightOperand.getExpectedRationalConstant() - assertThat(rootRightConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(1)) - assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(3)) - } + val poly39 = parseNumericExpressionWithAllErrors("2^(3-6)").toPolynomial() + assertThat(poly39).evaluatesToPlainTextThat().isEqualTo("1/8") + assertThat(poly39).hasTermCountThat().isEqualTo(1) + assertThat(poly39).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(8) + } + hasVariableCountThat().isEqualTo(0) + } - @Test - fun testParse_multiplyThenDivide_haveLeftToRightAssociativity() { - val result = MathExpressionParser.parseExpression("1*3/2", allowedVariables = listOf()) - - // Left-to-right associativity means the first encountered binary op is done first, and the - // second is done last (which means it's at the root). Expect the following tree: - // / - // * 2 - // 1 3 - val rootExpression = result.getExpectedSuccessfulExpression() - val rootDivOp = rootExpression.getExpectedBinaryOperationWithOperator(DIVIDE) - val rootLeftMulOp = rootDivOp.leftOperand.getExpectedBinaryOperationWithOperator(MULTIPLY) - val rootRightConstant = rootDivOp.rightOperand.getExpectedRationalConstant() - val innerOpLeftConstant = rootLeftMulOp.leftOperand.getExpectedRationalConstant() - val innerOpRightConstant = rootLeftMulOp.rightOperand.getExpectedRationalConstant() - assertThat(rootRightConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(1)) - assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(3)) - } + // x^-3 is not a valid polynomial (since polynomials can't have negative powers). + val poly40 = parseAlgebraicExpressionWithAllErrors("x^(3-6)").toPolynomial() + assertThat(poly40).isNotValidPolynomial() + + // 2^x is not a polynomial. + val poly41 = parseAlgebraicExpressionWithoutOptionalErrors("2^x").toPolynomial() + assertThat(poly41).isNotValidPolynomial() + + // 1/x is not a polynomial. + val poly42 = parseAlgebraicExpressionWithoutOptionalErrors("1/x").toPolynomial() + assertThat(poly42).isNotValidPolynomial() + + val poly43 = parseAlgebraicExpressionWithAllErrors("x/2").toPolynomial() + assertThat(poly43).evaluatesToPlainTextThat().isEqualTo("(1/2)x") + assertThat(poly43).hasTermCountThat().isEqualTo(1) + assertThat(poly43).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } - @Test - fun testParse_divideThenMultiply_haveLeftToRightAssociativity() { - val result = MathExpressionParser.parseExpression("1/3*2", allowedVariables = listOf()) - - // Left-to-right associativity means the first encountered binary op is done first, and the - // second is done last (which means it's at the root). Expect the following tree: - // * - // / 2 - // 1 3 - val rootExpression = result.getExpectedSuccessfulExpression() - val rootMulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val rootLeftDivOp = rootMulOp.leftOperand.getExpectedBinaryOperationWithOperator(DIVIDE) - val rootRightConstant = rootMulOp.rightOperand.getExpectedRationalConstant() - val innerOpLeftConstant = rootLeftDivOp.leftOperand.getExpectedRationalConstant() - val innerOpRightConstant = rootLeftDivOp.rightOperand.getExpectedRationalConstant() - assertThat(rootRightConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(1)) - assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(3)) - } + val poly44 = parseAlgebraicExpressionWithAllErrors("(x-3)/2").toPolynomial() + assertThat(poly44).evaluatesToPlainTextThat().isEqualTo("(1/2)x - 3/2") + assertThat(poly44).hasTermCountThat().isEqualTo(2) + assertThat(poly44).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly44).term(1).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isTrue() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(3) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(0) + } - @Test - fun testParse_twoExponentiations_haveRightToLeftAssociativity() { - val result = MathExpressionParser.parseExpression("1^3^2", allowedVariables = listOf()) - - // Right-to-left associativity means the opposite of left-to-right: always perform operations in - // the opposite order encountered. Expect the following tree: - // ^ - // 1 ^ - // 3 2 - val rootExpression = result.getExpectedSuccessfulExpression() - val rootExpOp = rootExpression.getExpectedBinaryOperationWithOperator(EXPONENTIATE) - val rootLeftConstant = rootExpOp.leftOperand.getExpectedRationalConstant() - val rootRightExpOp = rootExpOp.rightOperand.getExpectedBinaryOperationWithOperator(EXPONENTIATE) - val innerOpLeftConstant = rootRightExpOp.leftOperand.getExpectedRationalConstant() - val innerOpRightConstant = rootRightExpOp.rightOperand.getExpectedRationalConstant() - assertThat(rootLeftConstant).isEqualTo(createWholeNumberFraction(1)) - assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(3)) - assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(2)) - } + val poly45 = parseAlgebraicExpressionWithAllErrors("(x-1)(x+1)").toPolynomial() + assertThat(poly45).evaluatesToPlainTextThat().isEqualTo("x^2 - 1") + assertThat(poly45).hasTermCountThat().isEqualTo(2) + assertThat(poly45).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly45).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } - @Test - fun testParse_addThenMultiply_multiplyHappensFirst() { - val result = MathExpressionParser.parseExpression("1+3*2", allowedVariables = listOf()) - - // Operator precedence means ensure multiplication happens first, but keep the general order - // of operands. Expect the following tree: - // + - // 1 * - // 3 2 - val rootExpression = result.getExpectedSuccessfulExpression() - val rootAddOp = rootExpression.getExpectedBinaryOperationWithOperator(ADD) - val rootLeftConstant = rootAddOp.leftOperand.getExpectedRationalConstant() - val rootRightMulOp = rootAddOp.rightOperand.getExpectedBinaryOperationWithOperator(MULTIPLY) - val innerOpLeftConstant = rootRightMulOp.leftOperand.getExpectedRationalConstant() - val innerOpRightConstant = rootRightMulOp.rightOperand.getExpectedRationalConstant() - assertThat(rootLeftConstant).isEqualTo(createWholeNumberFraction(1)) - assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(3)) - assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(2)) - } + // √x is not a polynomial. + val poly46 = parseAlgebraicExpressionWithAllErrors("sqrt(x)").toPolynomial() + assertThat(poly46).isNotValidPolynomial() + + val poly47 = parseAlgebraicExpressionWithAllErrors("√(x^2)").toPolynomial() + assertThat(poly47).evaluatesToPlainTextThat().isEqualTo("x") + assertThat(poly47).hasTermCountThat().isEqualTo(1) + assertThat(poly47).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } - @Test - fun testParse_multiplyThenAdd_multiplyHappensFirst() { - val result = MathExpressionParser.parseExpression("1*3+2", allowedVariables = listOf()) - - // Operator precedence follows expression order. Expect the following tree: - // + - // * 2 - // 1 3 - val rootExpression = result.getExpectedSuccessfulExpression() - val rootAddOp = rootExpression.getExpectedBinaryOperationWithOperator(ADD) - val rootLeftMulOp = rootAddOp.leftOperand.getExpectedBinaryOperationWithOperator(MULTIPLY) - val rootRightConstant = rootAddOp.rightOperand.getExpectedRationalConstant() - val innerOpLeftConstant = rootLeftMulOp.leftOperand.getExpectedRationalConstant() - val innerOpRightConstant = rootLeftMulOp.rightOperand.getExpectedRationalConstant() - assertThat(rootRightConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(1)) - assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(3)) - } + val poly51 = parseAlgebraicExpressionWithAllErrors("√(x^2y^2)").toPolynomial() + assertThat(poly51).evaluatesToPlainTextThat().isEqualTo("xy") + assertThat(poly51).hasTermCountThat().isEqualTo(1) + assertThat(poly51).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } - @Test - fun testParse_multiplyThenExponentaite_exponentiationHappensFirst() { - val result = MathExpressionParser.parseExpression("1*3^2", allowedVariables = listOf()) - - // Operator precedence means ensure exponentiation happens first, but keep the general order - // of operands. Expect the following tree: - // * - // 1 ^ - // 3 2 - val rootExpression = result.getExpectedSuccessfulExpression() - val rootMulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val rootLeftConstant = rootMulOp.leftOperand.getExpectedRationalConstant() - val rootRightExpOp = rootMulOp.rightOperand.getExpectedBinaryOperationWithOperator(EXPONENTIATE) - val innerOpLeftConstant = rootRightExpOp.leftOperand.getExpectedRationalConstant() - val innerOpRightConstant = rootRightExpOp.rightOperand.getExpectedRationalConstant() - assertThat(rootLeftConstant).isEqualTo(createWholeNumberFraction(1)) - assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(3)) - assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(2)) - } + // A limitation in the current polynomial conversion is that sqrt(x) will fail due to it not + // have any polynomial representation. + val poly48 = parseAlgebraicExpressionWithAllErrors("√x^2").toPolynomial() + assertThat(poly48).isNotValidPolynomial() - @Test - fun testParse_exponentiateThenMultiply_exponentiationHappensFirst() { - val result = MathExpressionParser.parseExpression("1^3*2", allowedVariables = listOf()) - - // Operator precedence follows expression order. Expect the following tree: - // * - // ^ 2 - // 1 3 - val rootExpression = result.getExpectedSuccessfulExpression() - val rootMulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val rootLeftExpOp = rootMulOp.leftOperand.getExpectedBinaryOperationWithOperator(EXPONENTIATE) - val rootRightConstant = rootMulOp.rightOperand.getExpectedRationalConstant() - val innerOpLeftConstant = rootLeftExpOp.leftOperand.getExpectedRationalConstant() - val innerOpRightConstant = rootLeftExpOp.rightOperand.getExpectedRationalConstant() - assertThat(rootRightConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(1)) - assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(3)) - } + // √(x^2+2) may evaluate to a polynomial, but it requires factoring (which isn't yet supported). + val poly50 = parseAlgebraicExpressionWithAllErrors("√(x^2+2)").toPolynomial() + assertThat(poly50).isNotValidPolynomial() - @Test - fun testParse_addThenMultiply_withParentheses_addHappensFirst() { - val result = MathExpressionParser.parseExpression("(1+3)*2", allowedVariables = listOf()) - - // Parentheses override operator precedence so that addition happens first. Expect the following - // tree: - // * - // + 2 - // 1 3 - val rootExpression = result.getExpectedSuccessfulExpression() - val rootMulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val rootLeftAddOp = rootMulOp.leftOperand.getExpectedBinaryOperationWithOperator(ADD) - val rootRightConstant = rootMulOp.rightOperand.getExpectedRationalConstant() - val innerOpLeftConstant = rootLeftAddOp.leftOperand.getExpectedRationalConstant() - val innerOpRightConstant = rootLeftAddOp.rightOperand.getExpectedRationalConstant() - assertThat(rootRightConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(innerOpLeftConstant).isEqualTo(createWholeNumberFraction(1)) - assertThat(innerOpRightConstant).isEqualTo(createWholeNumberFraction(3)) - } + // Division by zero is undefined, so a polynomial can't be constructed. + val poly49 = parseAlgebraicExpressionWithoutOptionalErrors("(x+2)/0").toPolynomial() + assertThat(poly49).isNotValidPolynomial() - @Test - fun testParse_withNestedParentheses_deeperParenthesesEvaluateFirst() { - val result = MathExpressionParser.parseExpression("1^(2*(1-x))", allowedVariables = listOf("x")) - - // Nested parentheses happen before earlier parentheses. Expect the following tree: - // ^ - // 1 * - // 2 - - // 1 x - val rootExpression = result.getExpectedSuccessfulExpression() - val rootExpOp = rootExpression.getExpectedBinaryOperationWithOperator(EXPONENTIATE) - val rootLeftConstant = rootExpOp.leftOperand.getExpectedRationalConstant() - val rootRightMulOp = rootExpOp.rightOperand.getExpectedBinaryOperationWithOperator(MULTIPLY) - val mulLeftConstant = rootRightMulOp.leftOperand.getExpectedRationalConstant() - val mulRightSubOp = rootRightMulOp.rightOperand.getExpectedBinaryOperationWithOperator(SUBTRACT) - val subLeftConstant = mulRightSubOp.leftOperand.getExpectedRationalConstant() - val subRightVar = mulRightSubOp.rightOperand.getExpectedVariable() - assertThat(rootLeftConstant).isEqualTo(createWholeNumberFraction(1)) - assertThat(mulLeftConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(subLeftConstant).isEqualTo(createWholeNumberFraction(1)) - assertThat(subRightVar).isEqualTo("x") - } + val poly52 = parsePolynomialFromNumericExpression("1") + val poly53 = parsePolynomialFromNumericExpression("0") + assertThat(poly52).isNotEqualTo(poly53) - @Test - fun testParse_combineMultiplicationSubtractionAndNegation_negationHasHighestPrecedence() { - // See: https://wcipeg.com/wiki/Shunting_yard_algorithm#Unary_operators. Unary negation is - // usually treated as higher precedence. - val result = MathExpressionParser.parseExpression("10/-1*-2", allowedVariables = listOf()) - - // Expected tree: - // * - // / - - // 10 - 2 - // 1 - val rootExpression = result.getExpectedSuccessfulExpression() - val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val divOp = mulOp.leftOperand.getExpectedBinaryOperationWithOperator(DIVIDE) - val rightMulNegateOp = mulOp.rightOperand.getExpectedUnaryOperationWithOperator(NEGATE) - val rightMulNegatedConstant = rightMulNegateOp.operand.getExpectedRationalConstant() - val leftDivConstant = divOp.leftOperand.getExpectedRationalConstant() - val rightDivNegateOp = divOp.rightOperand.getExpectedUnaryOperationWithOperator(NEGATE) - val rightDivNegatedConstant = rightDivNegateOp.operand.getExpectedRationalConstant() - assertThat(rightMulNegatedConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(leftDivConstant).isEqualTo(createWholeNumberFraction(10)) - assertThat(rightDivNegatedConstant).isEqualTo(createWholeNumberFraction(1)) - } - @Test - fun test_negationWithExponentiation_exponentiationHasHigherPrecedence() { - val result = MathExpressionParser.parseExpression("-2^3^4", allowedVariables = listOf()) - - // Expected tree (note negation happens last since it's lower precedence): - // - - // ^ - // 2 ^ - // 3 4 - val rootExpression = result.getExpectedSuccessfulExpression() - val negOp = rootExpression.getExpectedUnaryOperationWithOperator(NEGATE) - val secondExpOp = negOp.operand.getExpectedBinaryOperationWithOperator(EXPONENTIATE) - val secondExpLeftConstant = secondExpOp.leftOperand.getExpectedRationalConstant() - val firstExpOp = secondExpOp.rightOperand.getExpectedBinaryOperationWithOperator(EXPONENTIATE) - val firstExpLeftConstant = firstExpOp.leftOperand.getExpectedRationalConstant() - val firstExpRightConstant = firstExpOp.rightOperand.getExpectedRationalConstant() - assertThat(secondExpLeftConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(firstExpLeftConstant).isEqualTo(createWholeNumberFraction(3)) - assertThat(firstExpRightConstant).isEqualTo(createWholeNumberFraction(4)) - } + val poly54 = parsePolynomialFromNumericExpression("1+2") + val poly55 = parsePolynomialFromNumericExpression("3") + assertThat(poly54).isEqualTo(poly55) - @Test - fun testParse_complexExpression_followsPemdasWithAssociativity() { - val result = MathExpressionParser.parseExpression( - "1+(13+12)/15+3*2-7/4^2^3*x+-(2*(y-3.14))", - allowedVariables = listOf("x", "y") - ) + val poly56 = parsePolynomialFromNumericExpression("1-2") + val poly57 = parsePolynomialFromNumericExpression("-1") + assertThat(poly56).isEqualTo(poly57) - // Look at past tests for associativity & precedence rules that are repeated here. Expect the - // following tree: - // + - // - - (negation) - // + * * - // + * / x 2 - - // 1 / 3 2 7 ^ y 3.14 - // + 15 4 ^ - // 13 12 2 3 - // Note that the enumeration below isn't done in evaluation order. The variables below are also - // preferring a leftward-leaning breadth-first enumeration. - val rootExpression = result.getExpectedSuccessfulExpression() - // Parse the root level: addition. - val lvl1Elem1Op = rootExpression.getExpectedBinaryOperationWithOperator(ADD) - // Next level: subtraction & negation. - val lvl2Elem1Op = lvl1Elem1Op.leftOperand.getExpectedBinaryOperationWithOperator(SUBTRACT) - val lvl2Elem2Op = lvl1Elem1Op.rightOperand.getExpectedUnaryOperationWithOperator(NEGATE) - // Next level: addition, multiplication, and multiplication. - val lvl3Elem1Op = lvl2Elem1Op.leftOperand.getExpectedBinaryOperationWithOperator(ADD) - val lvl3Elem2Op = lvl2Elem1Op.rightOperand.getExpectedBinaryOperationWithOperator(MULTIPLY) - val lvl3Elem3Op = lvl2Elem2Op.operand.getExpectedBinaryOperationWithOperator(MULTIPLY) - // Next level: addition, multiplication, division, variable x, constant 2, subtraction. - val lvl4Elem1Op = lvl3Elem1Op.leftOperand.getExpectedBinaryOperationWithOperator(ADD) - val lvl4Elem2Op = lvl3Elem1Op.rightOperand.getExpectedBinaryOperationWithOperator(MULTIPLY) - val lvl4Elem3Op = lvl3Elem2Op.leftOperand.getExpectedBinaryOperationWithOperator(DIVIDE) - val lvl4Elem4Var = lvl3Elem2Op.rightOperand.getExpectedVariable() - val lvl4Elem5Const = lvl3Elem3Op.leftOperand.getExpectedRationalConstant() - val lvl4Elem6Op = lvl3Elem3Op.rightOperand.getExpectedBinaryOperationWithOperator(SUBTRACT) - // Next level: 1, division, 3, 2, 7, exponentiation, y, 3.14. - val lvl5Elem1Const = lvl4Elem1Op.leftOperand.getExpectedRationalConstant() - val lvl5Elem2Op = lvl4Elem1Op.rightOperand.getExpectedBinaryOperationWithOperator(DIVIDE) - val lvl5Elem3Const = lvl4Elem2Op.leftOperand.getExpectedRationalConstant() - val lvl5Elem4Const = lvl4Elem2Op.rightOperand.getExpectedRationalConstant() - val lvl5Elem5Const = lvl4Elem3Op.leftOperand.getExpectedRationalConstant() - val lvl5Elem6Op = lvl4Elem3Op.rightOperand.getExpectedBinaryOperationWithOperator(EXPONENTIATE) - val lvl5Elem7Var = lvl4Elem6Op.leftOperand.getExpectedVariable() - val lvl5Elem8Const = lvl4Elem6Op.rightOperand.getExpectedIrrationalConstant() - // Next level: addition, 15, 4, exponentiation - val lvl6Elem1Op = lvl5Elem2Op.leftOperand.getExpectedBinaryOperationWithOperator(ADD) - val lvl6Elem2Const = lvl5Elem2Op.rightOperand.getExpectedRationalConstant() - val lvl6Elem3Const = lvl5Elem6Op.leftOperand.getExpectedRationalConstant() - val lvl6Elem4Op = lvl5Elem6Op.rightOperand.getExpectedBinaryOperationWithOperator(EXPONENTIATE) - // Final level: 13, 12, 2, 3 - val lvl7Elem1Const = lvl6Elem1Op.leftOperand.getExpectedRationalConstant() - val lvl7Elem2Const = lvl6Elem1Op.rightOperand.getExpectedRationalConstant() - val lvl7Elem3Const = lvl6Elem4Op.leftOperand.getExpectedRationalConstant() - val lvl7Elem4Const = lvl6Elem4Op.rightOperand.getExpectedRationalConstant() - - // Now, verify the constants and variables (cross reference with the level comments above & the - // tree to verify). - assertThat(lvl4Elem4Var).isEqualTo("x") - assertThat(lvl4Elem5Const).isEqualTo(createWholeNumberFraction(2)) - assertThat(lvl5Elem1Const).isEqualTo(createWholeNumberFraction(1)) - assertThat(lvl5Elem3Const).isEqualTo(createWholeNumberFraction(3)) - assertThat(lvl5Elem4Const).isEqualTo(createWholeNumberFraction(2)) - assertThat(lvl5Elem5Const).isEqualTo(createWholeNumberFraction(7)) - assertThat(lvl5Elem7Var).isEqualTo("y") - assertThat(lvl5Elem8Const).isWithin(1e-5).of(3.14) - assertThat(lvl6Elem2Const).isEqualTo(createWholeNumberFraction(15)) - assertThat(lvl6Elem3Const).isEqualTo(createWholeNumberFraction(4)) - assertThat(lvl7Elem1Const).isEqualTo(createWholeNumberFraction(13)) - assertThat(lvl7Elem2Const).isEqualTo(createWholeNumberFraction(12)) - assertThat(lvl7Elem3Const).isEqualTo(createWholeNumberFraction(2)) - assertThat(lvl7Elem4Const).isEqualTo(createWholeNumberFraction(3)) - } + val poly58 = parsePolynomialFromNumericExpression("2*3") + val poly59 = parsePolynomialFromNumericExpression("6") + assertThat(poly58).isEqualTo(poly59) - @Test - fun testParse_twoShortVariables_withoutOperator_impliesMultiplication() { - val result = MathExpressionParser.parseExpression("xy", allowedVariables = listOf("x", "y")) - - // Having two variables right next to each other implies multiplication. - val rootExpression = result.getExpectedSuccessfulExpression() - val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val leftVar = mulOp.leftOperand.getExpectedVariable() - val rightVar = mulOp.rightOperand.getExpectedVariable() - assertThat(leftVar).isEqualTo("x") - assertThat(rightVar).isEqualTo("y") - } + val poly60 = parsePolynomialFromNumericExpression("2^3") + val poly61 = parsePolynomialFromNumericExpression("8") + assertThat(poly60).isEqualTo(poly61) - @Test - fun testParse_constantWithShortVariable_withoutOperator_impliesMultiplication() { - val result = MathExpressionParser.parseExpression("2x", allowedVariables = listOf("x")) - - // Having a constant and a variable right next to each other implies multiplication. - val rootExpression = result.getExpectedSuccessfulExpression() - val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val leftConstant = mulOp.leftOperand.getExpectedRationalConstant() - val rightVar = mulOp.rightOperand.getExpectedVariable() - assertThat(leftConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(rightVar).isEqualTo("x") - } + val poly62 = parsePolynomialFromAlgebraicExpression("1+x") + val poly63 = parsePolynomialFromAlgebraicExpression("x+1") + assertThat(poly62).isEqualTo(poly63) - @Test - fun testParse_constantWithLongVariable_withoutOperator_impliesMultiplication() { - val result = MathExpressionParser.parseExpression( - "2lambda", allowedVariables = listOf("lambda") - ) + val poly64 = parsePolynomialFromAlgebraicExpression("y+x") + val poly65 = parsePolynomialFromAlgebraicExpression("x+y") + assertThat(poly64).isEqualTo(poly65) - // Having a constant and a variable right next to each other implies multiplication. - val rootExpression = result.getExpectedSuccessfulExpression() - val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val leftConstant = mulOp.leftOperand.getExpectedRationalConstant() - val rightVar = mulOp.rightOperand.getExpectedVariable() - assertThat(leftConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(rightVar).isEqualTo("lambda") - } + val poly66 = parsePolynomialFromAlgebraicExpression("(x+1)^2") + val poly67 = parsePolynomialFromAlgebraicExpression("x^2+2x+1") + assertThat(poly66).isEqualTo(poly67) - @Test - fun testParse_constantWithMultipleVariables_ambiguous_withoutOperator_impliesLongVarMult() { - val result = MathExpressionParser.parseExpression( - "2xyz", allowedVariables = listOf("x", "y", "z", "xyz") - ) + val poly68 = parsePolynomialFromAlgebraicExpression("(x+1)/2") + val poly69 = parsePolynomialFromAlgebraicExpression("x/2+(1/2)") + assertThat(poly68).isEqualTo(poly69) + + val poly70 = parsePolynomialFromAlgebraicExpression("x*2") + val poly71 = parsePolynomialFromAlgebraicExpression("2x") + assertThat(poly70).isEqualTo(poly71) - // The implied multiplication here is always on long variable since it's otherwise ambiguous. - val rootExpression = result.getExpectedSuccessfulExpression() - val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val leftConstant = mulOp.leftOperand.getExpectedRationalConstant() - val rightVar = mulOp.rightOperand.getExpectedVariable() - assertThat(leftConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(rightVar).isEqualTo("xyz") + val poly72 = parsePolynomialFromAlgebraicExpression("x(x+1)") + val poly73 = parsePolynomialFromAlgebraicExpression("x^2+x") + assertThat(poly72).isEqualTo(poly73) } - @Test - fun testParse_shortAndLongVariable_withoutOperator_failsToParse() { - val result = MathExpressionParser.parseExpression( - "wxyz", allowedVariables = listOf("w", "xyz") - ) + private fun createComparableOperationListFromNumericExpression(expression: String) = + parseNumericExpressionWithAllErrors(expression).toComparableOperationList() - // There can't be implied multiplication here since 'wxyz' looks like 1 variable. Note that if - // 'x', 'y', 'z' are separate allowed variables then the above *will* succeed in parsing an - // expression equivalent to w*x*y*z. - val failureReason = result.getExpectedFailedExpression() - assertThat(failureReason).contains("Encountered invalid identifier: wxyz") - } + private fun createComparableOperationListFromAlgebraicExpression(expression: String) = + parseAlgebraicExpressionWithAllErrors(expression).toComparableOperationList() - @Test - fun testParse_variableNextToConstant_withoutOperator_impliesMultiplication() { - val result = MathExpressionParser.parseExpression("x2", allowedVariables = listOf("x")) - - // Having a constant and a variable right next to each other implies multiplication. - val rootExpression = result.getExpectedSuccessfulExpression() - val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val leftVar = mulOp.leftOperand.getExpectedVariable() - val rightConstant = mulOp.rightOperand.getExpectedRationalConstant() - assertThat(leftVar).isEqualTo("x") - assertThat(rightConstant).isEqualTo(createWholeNumberFraction(2)) - } + private fun parsePolynomialFromNumericExpression(expression: String) = + parseNumericExpressionWithAllErrors(expression).toPolynomial() - @Test - fun testParse_polynomialWithoutOperators_createsCorrectExpressionTree() { - val result = MathExpressionParser.parseExpression("2x^-2", allowedVariables = listOf("x")) - - // Basic polynomial expressions should parse as expected. For the above expression, expect the - // tree: - // * - // 2 ^ - // x - - // 2 - // Having a constant and a variable right next to each other implies multiplication. - val rootExpression = result.getExpectedSuccessfulExpression() - val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val leftMulConstant = mulOp.leftOperand.getExpectedRationalConstant() - val rightExpOp = mulOp.rightOperand.getExpectedBinaryOperationWithOperator(EXPONENTIATE) - val leftExpVar = rightExpOp.leftOperand.getExpectedVariable() - val rightExpNegOp = rightExpOp.rightOperand.getExpectedUnaryOperationWithOperator(NEGATE) - val negOpConst = rightExpNegOp.operand.getExpectedRationalConstant() - assertThat(leftMulConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(leftExpVar).isEqualTo("x") - assertThat(negOpConst).isEqualTo(createWholeNumberFraction(2)) - } + private fun parsePolynomialFromAlgebraicExpression(expression: String) = + parseAlgebraicExpressionWithAllErrors(expression).toPolynomial() - @Test - fun testParse_multipleShortVariables_withoutOperators_impliesMultipleMultiplications() { - val result = - MathExpressionParser.parseExpression("xyz", allowedVariables = listOf("x", "y", "z")) - - // Having consecutive variables also implies multiplication. In this case, the expression uses - // left associativity, so expect the following tree: - // * - // * z - // x y - val rootExpression = result.getExpectedSuccessfulExpression() - val secondMulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val firstMulOp = secondMulOp.leftOperand.getExpectedBinaryOperationWithOperator(MULTIPLY) - val secondMulRightVar = secondMulOp.rightOperand.getExpectedVariable() - val firstMulLeftVar = firstMulOp.leftOperand.getExpectedVariable() - val firstMulRightVar = firstMulOp.rightOperand.getExpectedVariable() - assertThat(secondMulRightVar).isEqualTo("z") - assertThat(firstMulLeftVar).isEqualTo("x") - assertThat(firstMulRightVar).isEqualTo("y") - } + @DslMarker private annotation class ExpressionComparatorMarker - @Test - fun testParse_shortVariables_withAmbiguousLongVariable_noOperators_impliesSingleVariable() { - val result = MathExpressionParser.parseExpression( - "xyz", allowedVariables = listOf("x", "y", "z", "xyz") - ) + @DslMarker private annotation class ComparableOperationComparatorMarker - // 'xyz' is ambiguous in this case, but a single variable should be preferred since it's an - // exact match. - val rootExpression = result.getExpectedSuccessfulExpression() - val rootVar = rootExpression.getExpectedVariable() - assertThat(rootVar).isEqualTo("xyz") - } + // See: https://kotlinlang.org/docs/type-safe-builders.html. + private class MathExpressionSubject( + metadata: FailureMetadata, + private val actual: MathExpression + ) : LiteProtoSubject(metadata, actual) { + fun hasStructureThatMatches(init: ExpressionComparator.() -> Unit) { + // TODO: maybe verify that all aspects are verified? + ExpressionComparator.createFromExpression(actual).also(init) + } - @Test - fun testParse_shortVariables_withAmbiguousLongVariable_withOperator_hasMultipleVariables() { - val result = MathExpressionParser.parseExpression( - "x*yz", allowedVariables = listOf("x", "y", "z", "xyz") - ) + fun evaluatesToRationalThat(): FractionSubject = + assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.RATIONAL).rational) - // Unlike the above test, the single operator is sufficient to disambiguate the the x, y, z vs. - // xyz variable dilemma, so expect the following tree: - // * - // * z - // x y - val rootExpression = result.getExpectedSuccessfulExpression() - val secondMulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val firstMulOp = secondMulOp.leftOperand.getExpectedBinaryOperationWithOperator(MULTIPLY) - val secondMulRightVar = secondMulOp.rightOperand.getExpectedVariable() - val firstMulLeftVar = firstMulOp.leftOperand.getExpectedVariable() - val firstMulRightVar = firstMulOp.rightOperand.getExpectedVariable() - assertThat(secondMulRightVar).isEqualTo("z") - assertThat(firstMulLeftVar).isEqualTo("x") - assertThat(firstMulRightVar).isEqualTo("y") - } + fun evaluatesToIrrationalThat(): DoubleSubject = + assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.IRRATIONAL).irrational) - @Test - fun testParse_polynomialMultiplication_withoutOperator_impliesMultiplication() { - val result = MathExpressionParser.parseExpression("(x+1)(x+2)", allowedVariables = listOf("x")) - - // Having two polynomials right next to each other implies multiplication. Expect the tree: - // * - // + + - // x 1 x 2 - val rootExpression = result.getExpectedSuccessfulExpression() - val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val leftAddOp = mulOp.leftOperand.getExpectedBinaryOperationWithOperator(ADD) - val rightAddOp = mulOp.rightOperand.getExpectedBinaryOperationWithOperator(ADD) - val leftAddLeftVar = leftAddOp.leftOperand.getExpectedVariable() - val leftAddRightVar = leftAddOp.rightOperand.getExpectedRationalConstant() - val rightAddLeftVar = rightAddOp.leftOperand.getExpectedVariable() - val rightAddRightVar = rightAddOp.rightOperand.getExpectedRationalConstant() - assertThat(leftAddLeftVar).isEqualTo("x") - assertThat(leftAddRightVar).isEqualTo(createWholeNumberFraction(1)) - assertThat(rightAddLeftVar).isEqualTo("x") - assertThat(rightAddRightVar).isEqualTo(createWholeNumberFraction(2)) - } + fun evaluatesToIntegerThat(): IntegerSubject = + assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.INTEGER).integer) - @Test - fun testParse_constantAndPolynomialMultiplication_withoutOperator_impliesMultiplication() { - val result = MathExpressionParser.parseExpression("2(x+1)", allowedVariables = listOf("x")) - - // Having a constant and a polynomial right next to each other implies multiplication. Expect: - // * - // 2 + - // x 1 - val rootExpression = result.getExpectedSuccessfulExpression() - val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val leftConstant = mulOp.leftOperand.getExpectedRationalConstant() - val rightAddOp = mulOp.rightOperand.getExpectedBinaryOperationWithOperator(ADD) - val rightAddLeftVar = rightAddOp.leftOperand.getExpectedVariable() - val rightAddRightConstant = rightAddOp.rightOperand.getExpectedRationalConstant() - assertThat(leftConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(rightAddLeftVar).isEqualTo("x") - assertThat(rightAddRightConstant).isEqualTo(createWholeNumberFraction(1)) - } + fun convertsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = false)) - @Test - fun testParse_polynomialAndConstantMultiplication_withoutOperator_impliesMultiplication() { - val result = MathExpressionParser.parseExpression("(x+1)2", allowedVariables = listOf("x")) - - // Having a constant and a polynomial right next to each other implies multiplication. Expect: - // * - // + 2 - // x 1 - val rootExpression = result.getExpectedSuccessfulExpression() - val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val leftAddOp = mulOp.leftOperand.getExpectedBinaryOperationWithOperator(ADD) - val rightConstant = mulOp.rightOperand.getExpectedRationalConstant() - val leftAddLeftVar = leftAddOp.leftOperand.getExpectedVariable() - val leftAddRightConstant = leftAddOp.rightOperand.getExpectedRationalConstant() - assertThat(rightConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(leftAddLeftVar).isEqualTo("x") - assertThat(leftAddRightConstant).isEqualTo(createWholeNumberFraction(1)) - } + fun convertsWithFractionsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = true)) - @Test - fun testParse_variableAndPolynomialMultiplication_withoutOperator_impliesMultiplication() { - val result = MathExpressionParser.parseExpression("x(x+1)", allowedVariables = listOf("x")) - - // Having a constant and a polynomial right next to each other implies multiplication. Expect: - // * - // x + - // x 1 - val rootExpression = result.getExpectedSuccessfulExpression() - val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val leftVar = mulOp.leftOperand.getExpectedVariable() - val rightAddOp = mulOp.rightOperand.getExpectedBinaryOperationWithOperator(ADD) - val rightAddLeftVar = rightAddOp.leftOperand.getExpectedVariable() - val rightAddRightConstant = rightAddOp.rightOperand.getExpectedRationalConstant() - assertThat(leftVar).isEqualTo("x") - assertThat(rightAddLeftVar).isEqualTo("x") - assertThat(rightAddRightConstant).isEqualTo(createWholeNumberFraction(1)) - } + fun forHumanReadable(language: OppiaLanguage): HumanReadableStringChecker = + HumanReadableStringChecker(language, actual::toHumanReadableString) - @Test - fun testParse_polynomialAndVariableMultiplication_withoutOperator_impliesMultiplication() { - val result = MathExpressionParser.parseExpression("(x+1)x", allowedVariables = listOf("x")) - - // Having a constant and a polynomial right next to each other implies multiplication. Expect: - // * - // + 2 - // x 1 - val rootExpression = result.getExpectedSuccessfulExpression() - val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val leftAddOp = mulOp.leftOperand.getExpectedBinaryOperationWithOperator(ADD) - val rightVar = mulOp.rightOperand.getExpectedVariable() - val leftAddLeftVar = leftAddOp.leftOperand.getExpectedVariable() - val leftAddRightConstant = leftAddOp.rightOperand.getExpectedRationalConstant() - assertThat(rightVar).isEqualTo("x") - assertThat(leftAddLeftVar).isEqualTo("x") - assertThat(leftAddRightConstant).isEqualTo(createWholeNumberFraction(1)) - } + private fun evaluateAsReal(expectedType: Real.RealTypeCase): Real { + val real = actual.evaluateAsNumericExpression() + assertWithMessage("Failed to evaluate numeric expression").that(real).isNotNull() + assertWithMessage("Expected constant to evaluate to $expectedType") + .that(real?.realTypeCase) + .isEqualTo(expectedType) + return checkNotNull(real) // Just to remove the nullable operator; the actual check is above. + } - @Test - fun testParse_emptyLiteral_failsToResolveTree() { - val result = MathExpressionParser.parseExpression("", allowedVariables = listOf()) + private fun convertToLatex(divAsFraction: Boolean): String = actual.toRawLatex(divAsFraction) + + // TODO: update DSL to not have return values (since it's unnecessary). + @ExpressionComparatorMarker + class ExpressionComparator private constructor(private val expression: MathExpression) { + // TODO: convert to constant comparator? + fun constant(init: ConstantComparator.() -> Unit): ConstantComparator = + ConstantComparator.createFromExpression(expression).also(init) + + fun variable(init: VariableComparator.() -> Unit): VariableComparator = + VariableComparator.createFromExpression(expression).also(init) + + fun addition(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.ADD + ).also(init) + } + + fun subtraction(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.SUBTRACT + ).also(init) + } + + fun multiplication(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.MULTIPLY + ).also(init) + } + + fun division(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.DIVIDE + ).also(init) + } + + fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.EXPONENTIATE + ).also(init) + } + + fun negation(init: UnaryOperationComparator.() -> Unit): UnaryOperationComparator { + return UnaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathUnaryOperation.Operator.NEGATE + ).also(init) + } + + fun positive(init: UnaryOperationComparator.() -> Unit): UnaryOperationComparator { + return UnaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathUnaryOperation.Operator.POSITIVE + ).also(init) + } + + fun functionCallTo( + type: MathFunctionCall.FunctionType, + init: FunctionCallComparator.() -> Unit + ): FunctionCallComparator { + return FunctionCallComparator.createFromExpression( + expression, + expectedFunctionType = type + ).also(init) + } + + fun group(init: ExpressionComparator.() -> Unit): ExpressionComparator { + return createFromExpression(expression.group).also(init) + } + + internal companion object { + fun createFromExpression(expression: MathExpression): ExpressionComparator = + ExpressionComparator(expression) + } + } - val failureReason = result.getExpectedFailedExpression() - assertThat(failureReason).contains("Failed to resolve expression tree") - } + @ExpressionComparatorMarker + class ConstantComparator private constructor(private val constant: Real) { + fun withValueThat(): RealSubject = assertThat(constant) - @Test - fun testParse_twoConsecutiveConstants_failsToResolveTree() { - val result = MathExpressionParser.parseExpression("2 3", allowedVariables = listOf()) + internal companion object { + fun createFromExpression(expression: MathExpression): ConstantComparator { + assertThat(expression.expressionTypeCase).isEqualTo(CONSTANT) + return ConstantComparator(expression.constant) + } + } + } - val failureReason = result.getExpectedFailedExpression() - assertThat(failureReason).contains("Failed to resolve expression tree") - } + @ExpressionComparatorMarker + class VariableComparator private constructor(private val variableName: String) { + fun withNameThat(): StringSubject = assertThat(variableName) - @Test - fun testParse_twoConsecutiveConstants_withParentheses_impliesMultiplication() { - val result = MathExpressionParser.parseExpression("(2) (3)", allowedVariables = listOf()) - - // Parentheses fully change the meaning since it now looks like polynomial multiplication. - val rootExpression = result.getExpectedSuccessfulExpression() - val mulOp = rootExpression.getExpectedBinaryOperationWithOperator(MULTIPLY) - val leftConstant = mulOp.leftOperand.getExpectedRationalConstant() - val rightConstant = mulOp.rightOperand.getExpectedRationalConstant() - assertThat(leftConstant).isEqualTo(createWholeNumberFraction(2)) - assertThat(rightConstant).isEqualTo(createWholeNumberFraction(3)) - } + internal companion object { + fun createFromExpression(expression: MathExpression): VariableComparator { + assertThat(expression.expressionTypeCase).isEqualTo(VARIABLE) + return VariableComparator(expression.variable) + } + } + } - @Test - fun testParse_mismatchedOpenParenthesis_failsWithUnresolvedParenthesis() { - val result = MathExpressionParser.parseExpression("(", allowedVariables = listOf()) + @ExpressionComparatorMarker + class BinaryOperationComparator private constructor( + private val operation: MathBinaryOperation + ) { + fun leftOperand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(operation.leftOperand).also(init) + + fun rightOperand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(operation.rightOperand).also(init) + + internal companion object { + fun createFromExpression( + expression: MathExpression, + expectedOperator: MathBinaryOperation.Operator + ): BinaryOperationComparator { + assertThat(expression.expressionTypeCase).isEqualTo(BINARY_OPERATION) + assertWithMessage("Expected binary operation with operator: $expectedOperator") + .that(expression.binaryOperation.operator) + .isEqualTo(expectedOperator) + return BinaryOperationComparator(expression.binaryOperation) + } + } + } + + @ExpressionComparatorMarker + class UnaryOperationComparator private constructor( + private val operation: MathUnaryOperation + ) { + fun operand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(operation.operand).also(init) + + internal companion object { + fun createFromExpression( + expression: MathExpression, + expectedOperator: MathUnaryOperation.Operator + ): UnaryOperationComparator { + assertThat(expression.expressionTypeCase).isEqualTo(UNARY_OPERATION) + assertWithMessage("Expected unary operation with operator: $expectedOperator") + .that(expression.unaryOperation.operator) + .isEqualTo(expectedOperator) + return UnaryOperationComparator(expression.unaryOperation) + } + } + } - val failureReason = result.getExpectedFailedExpression() - assertThat(failureReason).contains("unexpected open parenthesis") + @ExpressionComparatorMarker + class FunctionCallComparator private constructor( + private val functionCall: MathFunctionCall + ) { + fun argument(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(functionCall.argument).also(init) + + internal companion object { + fun createFromExpression( + expression: MathExpression, + expectedFunctionType: MathFunctionCall.FunctionType + ): FunctionCallComparator { + assertThat(expression.expressionTypeCase).isEqualTo(FUNCTION_CALL) + assertWithMessage("Expected function call to: $expectedFunctionType") + .that(expression.functionCall.functionType) + .isEqualTo(expectedFunctionType) + return FunctionCallComparator(expression.functionCall) + } + } + } } - @Test - fun testParse_extraOpenParenthesis_failsWithUnresolvedParenthesis() { - val result = MathExpressionParser.parseExpression("((2)", allowedVariables = listOf()) + private class MathEquationSubject( + metadata: FailureMetadata, + private val actual: MathEquation + ) : LiteProtoSubject(metadata, actual) { + fun hasLeftHandSideThat(): MathExpressionSubject = assertThat(actual.leftSide) - val failureReason = result.getExpectedFailedExpression() - assertThat(failureReason).contains("unexpected open parenthesis") - } + fun hasRightHandSideThat(): MathExpressionSubject = assertThat(actual.rightSide) - @Test - fun testParse_mismatchedCloseParenthesis_failsWithUnresolvedParenthesis() { - val result = MathExpressionParser.parseExpression(")", allowedVariables = listOf()) + fun convertsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = false)) - val failureReason = result.getExpectedFailedExpression() - assertThat(failureReason).contains("unexpected close parenthesis") - } + fun convertsWithFractionsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = true)) - @Test - fun testParse_extraCloseParenthesis_failsWithUnresolvedParenthesis() { - val result = MathExpressionParser.parseExpression("(2))", allowedVariables = listOf()) + fun forHumanReadable(language: OppiaLanguage): HumanReadableStringChecker = + HumanReadableStringChecker(language, actual::toHumanReadableString) - val failureReason = result.getExpectedFailedExpression() - assertThat(failureReason).contains("unexpected close parenthesis") + private fun convertToLatex(divAsFraction: Boolean): String = actual.toRawLatex(divAsFraction) } - @Test - fun testParse_mismatchedCloseParenthesis_failsToResolveTree() { - val result = MathExpressionParser.parseExpression("()", allowedVariables = listOf()) + private class HumanReadableStringChecker( + private val language: OppiaLanguage, + private val maybeConvertToHumanReadableString: (OppiaLanguage, Boolean) -> String? + ) { + fun convertsToStringThat(): StringSubject = + assertThat(convertToHumanReadableString(language, /* divAsFraction= */ false)) - val failureReason = result.getExpectedFailedExpression() - assertThat(failureReason).contains("Failed to resolve expression tree") - } + fun convertsWithFractionsToStringThat(): StringSubject = + assertThat(convertToHumanReadableString(language, /* divAsFraction= */ true)) - @Test - fun testParse_twoConsecutiveBinaryOperators_failsWithMissingBinaryOperand() { - val result = MathExpressionParser.parseExpression("2**3", allowedVariables = listOf()) + fun doesNotConvertToString() { + assertWithMessage("Expected to not convert to: $language") + .that(maybeConvertToHumanReadableString(language, /* divAsFraction= */ false)) + .isNull() + } - val failureReason = result.getExpectedFailedExpression() - assertThat(failureReason).contains("Encountered binary operator with missing operand(s)") + private fun convertToHumanReadableString( + language: OppiaLanguage, divAsFraction: Boolean + ): String { + val readableString = maybeConvertToHumanReadableString(language, divAsFraction) + assertWithMessage("Expected to convert to: $language").that(readableString).isNotNull() + return checkNotNull(readableString) // Verified in the above assertion check. + } } - @Test - fun testParse_binaryOperator_missingRightOperand_failsWithMissingBinaryOperand() { - val result = MathExpressionParser.parseExpression("2*", allowedVariables = listOf()) + // TODO: move these to a common location. + private class FractionSubject( + metadata: FailureMetadata, + private val actual: Fraction + ) : LiteProtoSubject(metadata, actual) { + fun hasNegativePropertyThat(): BooleanSubject = assertThat(actual.isNegative) - val failureReason = result.getExpectedFailedExpression() - assertThat(failureReason).contains("Encountered binary operator with missing operand(s)") - } + fun hasWholeNumberThat(): IntegerSubject = assertThat(actual.wholeNumber) - @Test - fun testParse_binaryOperator_missingLeftOperand_failsWithMissingBinaryOperand() { - val result = MathExpressionParser.parseExpression("*2", allowedVariables = listOf()) + fun hasNumeratorThat(): IntegerSubject = assertThat(actual.numerator) - val failureReason = result.getExpectedFailedExpression() - assertThat(failureReason).contains("Encountered binary operator with missing operand(s)") + fun hasDenominatorThat(): IntegerSubject = assertThat(actual.denominator) + + fun evaluatesToRealThat(): DoubleSubject = assertThat(actual.toDouble()) } - @Test - fun testParse_binaryOperator_missingBothOperands_failsWithMissingBinaryOperand() { - val result = MathExpressionParser.parseExpression("*", allowedVariables = listOf()) + private class RealSubject( + metadata: FailureMetadata, + private val actual: Real + ) : LiteProtoSubject(metadata, actual) { + fun isRationalThat(): FractionSubject { + verifyTypeToBe(Real.RealTypeCase.RATIONAL) + return assertThat(actual.rational) + } - val failureReason = result.getExpectedFailedExpression() - assertThat(failureReason).contains("Encountered binary operator with missing operand(s)") - } + fun isIrrationalThat(): DoubleSubject { + verifyTypeToBe(Real.RealTypeCase.IRRATIONAL) + return assertThat(actual.irrational) + } - @Test - fun testParse_negation_missingOperand_failsWithMissingUnaryOperand() { - val result = MathExpressionParser.parseExpression("-", allowedVariables = listOf()) + fun isIntegerThat(): IntegerSubject { + verifyTypeToBe(Real.RealTypeCase.INTEGER) + return assertThat(actual.integer) + } - val failureReason = result.getExpectedFailedExpression() - assertThat(failureReason).contains("Encountered unary operator without operand") + private fun verifyTypeToBe(expected: Real.RealTypeCase) { + assertWithMessage("Expected real type to be $expected, not: ${actual.realTypeCase}") + .that(actual.realTypeCase) + .isEqualTo(expected) + } } - @Test - fun testParse_unknownUnarySymbol_failsWithUnexpectedSymbol() { - val result = MathExpressionParser.parseExpression("3!", allowedVariables = listOf()) + private class ComparableOperationListSubject( + metadata: FailureMetadata, + private val actual: ComparableOperationList + ) : LiteProtoSubject(metadata, actual) { + fun hasStructureThatMatches(init: ComparableOperationComparator.() -> Unit) { + ComparableOperationComparator.createFrom(actual.rootOperation).also(init) + } - val failureReason = result.getExpectedFailedExpression() - assertThat(failureReason).contains("unexpected symbol") - } + @ComparableOperationComparatorMarker + class ComparableOperationComparator private constructor( + private val operation: ComparableOperation + ) { + fun hasNegatedPropertyThat(): BooleanSubject = assertThat(operation.isNegated) - @Test - fun testParse_unknownBinarySymbol_failsWithUnexpectedSymbol() { - val result = MathExpressionParser.parseExpression("2%3", allowedVariables = listOf()) + fun hasInvertedPropertyThat(): BooleanSubject = assertThat(operation.isInverted) - val failureReason = result.getExpectedFailedExpression() - assertThat(failureReason).contains("unexpected symbol") - } + fun commutativeAccumulationWithType( + type: ComparableOperationList.CommutativeAccumulation.AccumulationType, + init: CommutativeAccumulationComparator.() -> Unit + ): CommutativeAccumulationComparator = + CommutativeAccumulationComparator.createFrom(type, operation).also(init) - @Test - fun testParse_withInvalidAllowedVariable_throwsException() { - val exception = assertThrows(IllegalArgumentException::class) { - MathExpressionParser.parseExpression("", allowedVariables = listOf("invalid!")) + fun nonCommutativeOperation( + init: NonCommutativeOperationComparator.() -> Unit + ): NonCommutativeOperationComparator = + NonCommutativeOperationComparator.createFrom(operation).also(init) + + fun constantTerm(init: ConstantTermComparator.() -> Unit): ConstantTermComparator = + ConstantTermComparator.createFrom(operation).also(init) + + fun variableTerm(init: VariableTermComparator.() -> Unit): VariableTermComparator = + VariableTermComparator.createFrom(operation).also(init) + + internal companion object { + fun createFrom(operation: ComparableOperation): ComparableOperationComparator = + ComparableOperationComparator(operation) + } } - assertThat(exception).hasMessageThat().contains("contains non-letters") - } + @ComparableOperationComparatorMarker + class CommutativeAccumulationComparator private constructor( + private val accumulation: ComparableOperationList.CommutativeAccumulation + ) { + fun hasOperandCountThat(): IntegerSubject = assertThat(accumulation.combinedOperationsCount) + + fun index( + index: Int, + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator { + return ComparableOperationComparator.createFrom( + accumulation.combinedOperationsList[index] + ).also(init) + } + + internal companion object { + fun createFrom( + type: ComparableOperationList.CommutativeAccumulation.AccumulationType, + operation: ComparableOperation + ): CommutativeAccumulationComparator { + assertThat(operation.comparisonTypeCase) + .isEqualTo(ComparisonTypeCase.COMMUTATIVE_ACCUMULATION) + assertThat(operation.commutativeAccumulation.accumulationType).isEqualTo(type) + return CommutativeAccumulationComparator(operation.commutativeAccumulation) + } + } + } - @Test - fun testParse_withEmptyAllowedVariable_throwsException() { - val exception = assertThrows(IllegalArgumentException::class) { - MathExpressionParser.parseExpression("", allowedVariables = listOf("")) + @ComparableOperationComparatorMarker + class NonCommutativeOperationComparator private constructor( + private val operation: ComparableOperationList.NonCommutativeOperation + ) { + fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + verifyTypeAs( + ComparableOperationList.NonCommutativeOperation.OperationTypeCase.EXPONENTIATION + ) + return BinaryOperationComparator.createFrom(operation.exponentiation).also(init) + } + + fun squareRootWithArgument( + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator { + verifyTypeAs(ComparableOperationList.NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT) + return ComparableOperationComparator.createFrom(operation.squareRoot).also(init) + } + + private fun verifyTypeAs( + type: ComparableOperationList.NonCommutativeOperation.OperationTypeCase + ) { + assertThat(operation.operationTypeCase).isEqualTo(type) + } + + internal companion object { + fun createFrom(operation: ComparableOperation): NonCommutativeOperationComparator { + assertThat(operation.comparisonTypeCase) + .isEqualTo(ComparisonTypeCase.NON_COMMUTATIVE_OPERATION) + return NonCommutativeOperationComparator(operation.nonCommutativeOperation) + } + } } - assertThat(exception).hasMessageThat().contains("empty identifier") - } + @ComparableOperationComparatorMarker + class BinaryOperationComparator private constructor( + private val operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation + ) { + fun leftOperand( + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator = + ComparableOperationComparator.createFrom(operation.leftOperand).also(init) + + fun rightOperand( + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator = + ComparableOperationComparator.createFrom(operation.rightOperand).also(init) + + internal companion object { + fun createFrom( + operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation + ): BinaryOperationComparator = BinaryOperationComparator(operation) + } + } - @Test - fun testParse_expressionWithUndefinedVariable_failsWithUnexpectedIdentifier() { - val result = MathExpressionParser.parseExpression("x", allowedVariables = listOf()) + @ComparableOperationComparatorMarker + class ConstantTermComparator private constructor( + private val constant: Real + ) { + fun withValueThat(): RealSubject = assertThat(constant) + + internal companion object { + fun createFrom(operation: ComparableOperation): ConstantTermComparator { + assertThat(operation.comparisonTypeCase).isEqualTo(ComparisonTypeCase.CONSTANT_TERM) + return ConstantTermComparator(operation.constantTerm) + } + } + } - val failureReason = result.getExpectedFailedExpression() - assertThat(failureReason).contains("invalid identifier: x") + @ComparableOperationComparatorMarker + class VariableTermComparator private constructor( + private val variableName: String + ) { + fun withNameThat(): StringSubject = assertThat(variableName) + + internal companion object { + fun createFrom(operation: ComparableOperation): VariableTermComparator { + assertThat(operation.comparisonTypeCase).isEqualTo(ComparisonTypeCase.VARIABLE_TERM) + return VariableTermComparator(operation.variableTerm) + } + } + } } - @Test - fun testParse_expressionWithInvalidCharacter_failsWithUnexpectedSymbol() { - val result = MathExpressionParser.parseExpression("∫", allowedVariables = listOf()) + private class PolynomialSubject( + metadata: FailureMetadata, + private val actual: Polynomial? + ) : LiteProtoSubject(metadata, actual) { + private val nonNullActual by lazy { + checkNotNull(actual) { + "Expected polynomial to be defined, not null (is the expression/equation not a valid" + + " polynomial?)" + } + } - val failureReason = result.getExpectedFailedExpression() - assertThat(failureReason).contains("unexpected symbol") - } + fun isNotValidPolynomial() { + assertWithMessage( + "Expected polynomial to be undefined, but was: ${actual?.toPlainText()}" + ).that(actual).isNull() + } - private fun ParseResult.getExpectedSuccessfulExpression(): MathExpression { - assertThat(this).isInstanceOf(Success::class.java) - return (this as Success).mathExpression - } + fun isConstantThat(): RealSubject { + assertWithMessage("Expected polynomial to be constant: $nonNullActual") + .that(nonNullActual.isConstant()) + .isTrue() + return assertThat(nonNullActual.getConstant()) + } - private fun ParseResult.getExpectedFailedExpression(): String { - assertThat(this).isInstanceOf(Failure::class.java) - return (this as Failure).failureReason - } + fun hasTermCountThat(): IntegerSubject = assertThat(nonNullActual.termCount) - private fun MathExpression.getExpectedConstant(): Real { - return getExpectedType(MathExpression::getConstant, CONSTANT) - } + fun term(index: Int): PolynomialTermSubject = assertThat(nonNullActual.termList[index]) - private fun MathExpression.getExpectedRationalConstant(): Fraction { - val constant = getExpectedType(MathExpression::getConstant, CONSTANT) - assertThat(constant.realTypeCase).isEqualTo(RATIONAL) - return constant.rational + fun evaluatesToPlainTextThat(): StringSubject = assertThat(nonNullActual.toPlainText()) } - private fun MathExpression.getExpectedIrrationalConstant(): Double { - val constant = getExpectedType(MathExpression::getConstant, CONSTANT) - assertThat(constant.realTypeCase).isEqualTo(IRRATIONAL) - return constant.irrational - } + private class PolynomialTermSubject( + metadata: FailureMetadata, + private val actual: Polynomial.Term + ) : LiteProtoSubject(metadata, actual) { + fun hasCoefficientThat(): RealSubject = assertThat(actual.coefficient) - private fun MathExpression.getExpectedVariable(): String { - return getExpectedType(MathExpression::getVariable, VARIABLE) - } + fun hasVariableCountThat(): IntegerSubject = assertThat(actual.variableCount) - private fun MathExpression.getExpectedUnaryOperation(): MathUnaryOperation { - return getExpectedType(MathExpression::getUnaryOperation, UNARY_OPERATION) + fun variable(index: Int): PolynomialTermVariableSubject = + assertThat(actual.variableList[index]) } - private fun MathExpression.getExpectedUnaryOperationWithOperator( - operator: MathUnaryOperation.Operator - ): MathUnaryOperation { - val expectedOp = getExpectedType(MathExpression::getUnaryOperation, UNARY_OPERATION) - assertThat(expectedOp.operator).isEqualTo(operator) - return expectedOp - } + private class PolynomialTermVariableSubject( + metadata: FailureMetadata, + private val actual: Polynomial.Term.Variable + ) : LiteProtoSubject(metadata, actual) { + fun hasNameThat(): StringSubject = assertThat(actual.name) - private fun MathExpression.getExpectedBinaryOperation(): MathBinaryOperation { - return getExpectedType(MathExpression::getBinaryOperation, BINARY_OPERATION) + fun hasPowerThat(): IntegerSubject = assertThat(actual.power) } - private fun MathExpression.getExpectedBinaryOperationWithOperator( - operator: MathBinaryOperation.Operator - ): MathBinaryOperation { - val expectedOp = getExpectedType(MathExpression::getBinaryOperation, BINARY_OPERATION) - assertThat(expectedOp.operator).isEqualTo(operator) - return expectedOp - } + private companion object { + // TODO: fix helper API. - private fun MathExpression.getExpectedType( - typeRetriever: MathExpression.() -> T, - expectedType: ExpressionTypeCase - ): T { - assertThat(expressionTypeCase).isEqualTo(expectedType) - return typeRetriever() - } + private fun expectFailureWhenParsingNumericExpression(expression: String): MathParsingError { + val result = parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseNumericExpressionWithoutOptionalErrors(expression: String): MathExpression { + return (parseNumericExpressionInternal(expression, ErrorCheckingMode.REQUIRED_ONLY) as MathParsingResult.Success).result + } + + private fun parseNumericExpressionWithAllErrors(expression: String): MathExpression { + return (parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) as MathParsingResult.Success).result + } + + private fun parseNumericExpressionInternal( + expression: String, errorCheckingMode: ErrorCheckingMode + ): MathParsingResult { + return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) + } + + private fun expectFailureWhenParsingAlgebraicExpression( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingError { + val result = + parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseAlgebraicExpressionWithoutOptionalErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + return (parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables) as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + return (parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) + } + + private fun expectFailureWhenParsingAlgebraicEquation(expression: String): MathParsingError { + val result = parseAlgebraicEquationInternal(expression, ErrorCheckingMode.ALL_ERRORS) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseAlgebraicEquationWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathEquation { + return (parseAlgebraicEquationInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) as MathParsingResult.Success).result + } + + private fun parseAlgebraicEquationInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicEquation( + expression, allowedVariables, errorCheckingMode + ) + } + + private fun assertThat(actual: MathExpression): MathExpressionSubject = + assertAbout(::MathExpressionSubject).that(actual) + + private fun assertThat(actual: MathEquation): MathEquationSubject = + assertAbout(::MathEquationSubject).that(actual) + + private fun assertThat(actual: Fraction): FractionSubject = + assertAbout(::FractionSubject).that(actual) + + private fun assertThat(actual: Real): RealSubject = assertAbout(::RealSubject).that(actual) + + private fun assertThat(actual: ComparableOperationList): ComparableOperationListSubject = + assertAbout(::ComparableOperationListSubject).that(actual) + + private fun assertThat(actual: Polynomial?): PolynomialSubject = + assertAbout(::PolynomialSubject).that(actual) + + private fun assertThat(actual: Polynomial.Term): PolynomialTermSubject = + assertAbout(::PolynomialTermSubject).that(actual) - private fun createWholeNumberFraction(value: Int): Fraction { - return Fraction.newBuilder().setWholeNumber(value).setDenominator(1).build() + private fun assertThat(actual: Polynomial.Term.Variable): PolynomialTermVariableSubject = + assertAbout(::PolynomialTermVariableSubject).that(actual) } } diff --git a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt index 0203b9eb883..ac7e6556b9c 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt @@ -10,7 +10,7 @@ import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.util.math.MathTokenizer2.Companion.Token +import org.oppia.android.util.math.MathTokenizer.Companion.Token import org.robolectric.annotation.LooperMode /** Tests for [MathTokenizer]. */ @@ -20,27 +20,28 @@ class MathTokenizerTest { @Test fun testLotsOfCases() { // TODO: split this up - val tokens1 = MathTokenizer2.tokenize(" ").toList() + // testTokenize_emptyString_producesNoTokens + val tokens1 = MathTokenizer.tokenize(" ").toList() assertThat(tokens1).isEmpty() - val tokens2 = MathTokenizer2.tokenize(" 2 ").toList() + val tokens2 = MathTokenizer.tokenize(" 2 ").toList() assertThat(tokens2).hasSize(1) assertThat(tokens2.first()).isPositiveIntegerWhoseValue().isEqualTo(2) - val tokens3 = MathTokenizer2.tokenize(" 2.5 ").toList() + val tokens3 = MathTokenizer.tokenize(" 2.5 ").toList() assertThat(tokens3).hasSize(1) assertThat(tokens3.first()).isPositiveRealNumberWhoseValue().isWithin(1e-5).of(2.5) - val tokens4 = MathTokenizer2.tokenize(" x ").toList() + val tokens4 = MathTokenizer.tokenize(" x ").toList() assertThat(tokens4).hasSize(1) assertThat(tokens4.first()).isVariableWhoseName().isEqualTo("x") - val tokens5 = MathTokenizer2.tokenize(" z x ").toList() + val tokens5 = MathTokenizer.tokenize(" z x ").toList() assertThat(tokens5).hasSize(2) assertThat(tokens5[0]).isVariableWhoseName().isEqualTo("z") assertThat(tokens5[1]).isVariableWhoseName().isEqualTo("x") - val tokens6 = MathTokenizer2.tokenize("2^3^2").toList() + val tokens6 = MathTokenizer.tokenize("2^3^2").toList() assertThat(tokens6).hasSize(5) assertThat(tokens6[0]).isPositiveIntegerWhoseValue().isEqualTo(2) assertThat(tokens6[1]).isExponentiationSymbol() @@ -48,21 +49,21 @@ class MathTokenizerTest { assertThat(tokens6[3]).isExponentiationSymbol() assertThat(tokens6[4]).isPositiveIntegerWhoseValue().isEqualTo(2) - val tokens7 = MathTokenizer2.tokenize("sqrt(2)").toList() + val tokens7 = MathTokenizer.tokenize("sqrt(2)").toList() assertThat(tokens7).hasSize(4) assertThat(tokens7[0]).isFunctionWhoseName().isEqualTo("sqrt") assertThat(tokens7[1]).isLeftParenthesisSymbol() assertThat(tokens7[2]).isPositiveIntegerWhoseValue().isEqualTo(2) assertThat(tokens7[3]).isRightParenthesisSymbol() - val tokens8 = MathTokenizer2.tokenize("sqr(2)").toList() + val tokens8 = MathTokenizer.tokenize("sqr(2)").toList() assertThat(tokens8).hasSize(4) - assertThat(tokens8[0]).isInvalidToken() + assertThat(tokens8[0]).isIncompleteFunctionName() assertThat(tokens8[1]).isLeftParenthesisSymbol() assertThat(tokens8[2]).isPositiveIntegerWhoseValue().isEqualTo(2) assertThat(tokens8[3]).isRightParenthesisSymbol() - val tokens9 = MathTokenizer2.tokenize("xyz(2)").toList() + val tokens9 = MathTokenizer.tokenize("xyz(2)").toList() assertThat(tokens9).hasSize(6) assertThat(tokens9[0]).isVariableWhoseName().isEqualTo("x") assertThat(tokens9[1]).isVariableWhoseName().isEqualTo("y") @@ -71,16 +72,16 @@ class MathTokenizerTest { assertThat(tokens9[4]).isPositiveIntegerWhoseValue().isEqualTo(2) assertThat(tokens9[5]).isRightParenthesisSymbol() - val tokens10 = MathTokenizer2.tokenize("732").toList() + val tokens10 = MathTokenizer.tokenize("732").toList() assertThat(tokens10).hasSize(1) assertThat(tokens10.first()).isPositiveIntegerWhoseValue().isEqualTo(732) - val tokens11 = MathTokenizer2.tokenize("73 2").toList() + val tokens11 = MathTokenizer.tokenize("73 2").toList() assertThat(tokens11).hasSize(2) assertThat(tokens11[0]).isPositiveIntegerWhoseValue().isEqualTo(73) assertThat(tokens11[1]).isPositiveIntegerWhoseValue().isEqualTo(2) - val tokens12 = MathTokenizer2.tokenize("1*2-3+4^7-8/3*2+7").toList() + val tokens12 = MathTokenizer.tokenize("1*2-3+4^7-8/3*2+7").toList() assertThat(tokens12).hasSize(17) assertThat(tokens12[0]).isPositiveIntegerWhoseValue().isEqualTo(1) assertThat(tokens12[1]).isMultiplySymbol() @@ -100,7 +101,7 @@ class MathTokenizerTest { assertThat(tokens12[15]).isPlusSymbol() assertThat(tokens12[16]).isPositiveIntegerWhoseValue().isEqualTo(7) - val tokens13 = MathTokenizer2.tokenize("x = √2 × 7 ÷ 4").toList() + val tokens13 = MathTokenizer.tokenize("x = √2 × 7 ÷ 4").toList() assertThat(tokens13).hasSize(8) assertThat(tokens13[0]).isVariableWhoseName().isEqualTo("x") assertThat(tokens13[1]).isEqualsSymbol() @@ -112,535 +113,6 @@ class MathTokenizerTest { assertThat(tokens13[7]).isPositiveIntegerWhoseValue().isEqualTo(4) } - /*private val ALLOWED_XYZ_VARIABLES = listOf("x", "y", "z") - private val ALLOWED_XYZ_WITH_LAMBDA_VARIABLES = ALLOWED_XYZ_VARIABLES + listOf("lambda") - private val ALLOWED_XYZ_WITH_COMBINED_XYZ_VARIABLES = ALLOWED_XYZ_VARIABLES + listOf("xyz") - - @Test - fun testTokenize_emptyString_producesNoTokens() { - val tokens = MathTokenizer.tokenize("", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).isEmpty() - } - - @Test - fun testTokenize_wholeNumber_oneDigit_producesWholeNumberToken() { - val tokens = MathTokenizer.tokenize("1", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(WholeNumber::class.java) - assertThat((tokens.first() as WholeNumber).value).isEqualTo(1) - } - - @Test - fun testTokenize_wholeNumber_multipleDigits_producesWholeNumberToken() { - val tokens = MathTokenizer.tokenize("913", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(WholeNumber::class.java) - assertThat((tokens.first() as WholeNumber).value).isEqualTo(913) - } - - @Test - fun testTokenize_wholeNumber_zeroLeadingNumber_producesCorrectBase10WholeNumberToken() { - val tokens = MathTokenizer.tokenize("0913", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(WholeNumber::class.java) - assertThat((tokens.first() as WholeNumber).value).isEqualTo(913) - } - - @Test - fun testTokenize_decimalNumber_decimalLessThanOne_noZero_producesCorrectDecimalNumberToken() { - val tokens = MathTokenizer.tokenize(".14", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(DecimalNumber::class.java) - assertThat((tokens.first() as DecimalNumber).value).isWithin(1e-5).of(0.14) - } - - @Test - fun testTokenize_decimalNumber_decimalLessThanOne_withZero_producesCorrectDecimalNumberToken() { - val tokens = MathTokenizer.tokenize("0.14", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(DecimalNumber::class.java) - assertThat((tokens.first() as DecimalNumber).value).isWithin(1e-5).of(0.14) - } - - @Test - fun testTokenize_decimalNumber_decimalGreaterThanOne_producesCorrectDecimalNumberToken() { - val tokens = MathTokenizer.tokenize("3.14", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(DecimalNumber::class.java) - assertThat((tokens.first() as DecimalNumber).value).isWithin(1e-5).of(3.14) - } - - @Test - fun testTokenize_decimalNumber_decimalPointOnly_producesInvalidToken() { - val tokens = MathTokenizer.tokenize(".", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(InvalidToken::class.java) - assertThat((tokens.first() as InvalidToken).token).isEqualTo(".") - } - - @Test - fun testTokenize_openParenthesis_producesOpenParenthesisToken() { - val tokens = MathTokenizer.tokenize("(", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(OpenParenthesis::class.java) - } - - @Test - fun testTokenize_closeParenthesis_producesCloseParenthesisToken() { - val tokens = MathTokenizer.tokenize(")", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(CloseParenthesis::class.java) - } - - @Test - fun testTokenize_plusSign_producesOperatorToken() { - val tokens = MathTokenizer.tokenize("+", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(Operator::class.java) - assertThat((tokens.first() as Operator).operator).isEqualTo('+') - } - - @Test - fun testTokenize_minusSign_producesOperatorToken() { - val tokens = MathTokenizer.tokenize("-", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(Operator::class.java) - assertThat((tokens.first() as Operator).operator).isEqualTo('-') - } - - @Test - fun testTokenize_asterisk_producesOperatorToken() { - val tokens = MathTokenizer.tokenize("*", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(Operator::class.java) - assertThat((tokens.first() as Operator).operator).isEqualTo('*') - } - - @Test - fun testTokenize_formalMultiplicationSign_producesAsteriskOperatorToken() { - val tokens = MathTokenizer.tokenize("×", ALLOWED_XYZ_VARIABLES).toList() - - // The formal math multiplication symbol is translated to the conventional one for simplicity. - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(Operator::class.java) - assertThat((tokens.first() as Operator).operator).isEqualTo('*') - } - - @Test - fun testTokenize_forwardSlash_producesOperatorToken() { - val tokens = MathTokenizer.tokenize("/", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(Operator::class.java) - assertThat((tokens.first() as Operator).operator).isEqualTo('/') - } - - @Test - fun testTokenize_formalDivisionSign_producesForwardSlashOperatorToken() { - val tokens = MathTokenizer.tokenize("÷", ALLOWED_XYZ_VARIABLES).toList() - - // The formal math division symbol is translated to the conventional one for simplicity. - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(Operator::class.java) - assertThat((tokens.first() as Operator).operator).isEqualTo('/') - } - - @Test - fun testTokenize_caret_producesOperatorToken() { - val tokens = MathTokenizer.tokenize("^", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(Operator::class.java) - assertThat((tokens.first() as Operator).operator).isEqualTo('^') - } - - @Test - fun testTokenize_exclamation_producesInvalidToken() { - val tokens = MathTokenizer.tokenize("!", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(InvalidToken::class.java) - assertThat((tokens.first() as InvalidToken).token).isEqualTo("!") - } - - @Test - fun testTokenize_validIdentifier_withAllowedIds_producesIdentifierToken() { - val tokens = MathTokenizer.tokenize("x", allowedIdentifiers = listOf("x")).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(Identifier::class.java) - assertThat((tokens.first() as Identifier).name).isEqualTo("x") - } - - @Test - fun testTokenize_validIdentifier_withNoIdentifiersProvided_producesInvalidIdentifierToken() { - val tokens = MathTokenizer.tokenize("x", allowedIdentifiers = listOf()).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(InvalidIdentifier::class.java) - assertThat((tokens.first() as InvalidIdentifier).name).isEqualTo("x") - } - - @Test - fun testTokenize_withInvalidAllowedIdentifiers_throwsException() { - val exception = assertThrows(IllegalArgumentException::class) { - MathTokenizer.tokenize("x", allowedIdentifiers = listOf("valid", "invalid!")).toList() - } - - assertThat(exception).hasMessageThat().contains("contains non-letters: invalid!") - } - - @Test - fun testTokenize_withEmptyAllowedIdentifier_throwsException() { - val exception = assertThrows(IllegalArgumentException::class) { - MathTokenizer.tokenize("x", allowedIdentifiers = listOf("valid", "")).toList() - } - - assertThat(exception).hasMessageThat().contains("Encountered empty identifier") - } - - @Test - fun testTokenize_withAllowedIdentifiers_producesIdentifierToken() { - val tokens = MathTokenizer.tokenize("z", allowedIdentifiers = listOf("z")).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(Identifier::class.java) - assertThat((tokens.first() as Identifier).name).isEqualTo("z") - } - - @Test - fun testTokenize_expressionWithIdLowercase_withAllowedIdentifiersUpper_producesIdToken() { - val tokens = MathTokenizer.tokenize("z", allowedIdentifiers = listOf("Z")).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(Identifier::class.java) - assertThat((tokens.first() as Identifier).name).isEqualTo("z") - } - - @Test - fun testTokenize_expressionWithIdUppercase_withAllowedIdentifiersLower_producesIdToken() { - val tokens = MathTokenizer.tokenize("Z", allowedIdentifiers = listOf("z")).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(Identifier::class.java) - assertThat((tokens.first() as Identifier).name).isEqualTo("z") - } - - @Test - fun testTokenize_expressionWithIdUppercase_withAllowedIdentifiersUpper_producesIdToken() { - val tokens = MathTokenizer.tokenize("Z", allowedIdentifiers = listOf("Z")).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(Identifier::class.java) - assertThat((tokens.first() as Identifier).name).isEqualTo("z") - } - - @Test - fun testTokenize_greekLetterIdentifier_withAllowedIdentifiers_producesIdentifierToken() { - val tokens = MathTokenizer.tokenize("π", allowedIdentifiers = listOf("π")).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(Identifier::class.java) - assertThat((tokens.first() as Identifier).name).isEqualTo("π") - } - - @Test - fun testTokenize_greekLetterIdentifier_withAllowedIdentifiersUppercase_producesIdentifierToken() { - val tokens = MathTokenizer.tokenize("π", allowedIdentifiers = listOf("Π")).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(Identifier::class.java) - assertThat((tokens.first() as Identifier).name).isEqualTo("π") - } - - @Test - fun testTokenize_multipleIdentifiers_withoutSpaces_producesIdentifierTokensForEachInOrder() { - val tokens = MathTokenizer.tokenize("xyz", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(3) - assertThat(tokens[0]).isInstanceOf(Identifier::class.java) - assertThat(tokens[1]).isInstanceOf(Identifier::class.java) - assertThat(tokens[2]).isInstanceOf(Identifier::class.java) - assertThat((tokens[0] as Identifier).name).isEqualTo("x") - assertThat((tokens[1] as Identifier).name).isEqualTo("y") - assertThat((tokens[2] as Identifier).name).isEqualTo("z") - } - - @Test - fun testTokenize_validMultiWordIdentifier_producesSingleIdentifierToken() { - val tokens = MathTokenizer.tokenize("lambda", allowedIdentifiers = listOf("lambda")).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(Identifier::class.java) - assertThat((tokens.first() as Identifier).name).isEqualTo("lambda") - } - - @Test - fun testTokenize_invalidMultiWordIdentifier_missingFromAllowedList_producesInvalidIdToken() { - val tokens = MathTokenizer.tokenize("xyz", allowedIdentifiers = listOf()).toList() - - // Note that even though 'x' and 'y' are valid single-letter variables, because 'z' is - // encountered the whole set of letters is considered a single invalid variable. - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(InvalidIdentifier::class.java) - assertThat((tokens.first() as InvalidIdentifier).name).isEqualTo("xyz") - } - - @Test - fun testTokenize_multipleIdentifiers_singleLetter_withSpaces_producesIdTokensForEachInOrder() { - val tokens = MathTokenizer.tokenize("x y z", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(3) - assertThat(tokens[0]).isInstanceOf(Identifier::class.java) - assertThat(tokens[1]).isInstanceOf(Identifier::class.java) - assertThat(tokens[2]).isInstanceOf(Identifier::class.java) - assertThat((tokens[0] as Identifier).name).isEqualTo("x") - assertThat((tokens[1] as Identifier).name).isEqualTo("y") - assertThat((tokens[2] as Identifier).name).isEqualTo("z") - } - - @Test - fun testTokenize_multipleIdentifiers_multiLetter_withSpaces_producesIdTokensForEachInOrder() { - val tokens = MathTokenizer.tokenize("abc def", listOf("abc", "def")).toList() - - assertThat(tokens).hasSize(2) - assertThat(tokens[0]).isInstanceOf(Identifier::class.java) - assertThat(tokens[1]).isInstanceOf(Identifier::class.java) - assertThat((tokens[0] as Identifier).name).isEqualTo("abc") - assertThat((tokens[1] as Identifier).name).isEqualTo("def") - } - - @Test - fun testTokenize_multipleIdentifiers_mixed_withSpaces_producesIdTokensForEachInOrder() { - val tokens = MathTokenizer.tokenize("a lambda", listOf("a", "lambda")).toList() - - assertThat(tokens).hasSize(2) - assertThat(tokens[0]).isInstanceOf(Identifier::class.java) - assertThat(tokens[1]).isInstanceOf(Identifier::class.java) - assertThat((tokens[0] as Identifier).name).isEqualTo("a") - assertThat((tokens[1] as Identifier).name).isEqualTo("lambda") - } - - @Test - fun testTokenize_multiplyTwoVariables_singleLetter_producesCorrectTokens() { - val tokens = MathTokenizer.tokenize("x*y", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(3) - assertThat(tokens[0]).isInstanceOf(Identifier::class.java) - assertThat(tokens[1]).isInstanceOf(Operator::class.java) - assertThat(tokens[2]).isInstanceOf(Identifier::class.java) - assertThat((tokens[0] as Identifier).name).isEqualTo("x") - assertThat((tokens[1] as Operator).operator).isEqualTo('*') - assertThat((tokens[2] as Identifier).name).isEqualTo("y") - } - - @Test - fun testTokenize_multiplyTwoVariables_multiLetter_producesCorrectTokens() { - val tokens = MathTokenizer.tokenize("abc*def", listOf("abc", "def")).toList() - - assertThat(tokens).hasSize(3) - assertThat(tokens[0]).isInstanceOf(Identifier::class.java) - assertThat(tokens[1]).isInstanceOf(Operator::class.java) - assertThat(tokens[2]).isInstanceOf(Identifier::class.java) - assertThat((tokens[0] as Identifier).name).isEqualTo("abc") - assertThat((tokens[1] as Operator).operator).isEqualTo('*') - assertThat((tokens[2] as Identifier).name).isEqualTo("def") - } - - @Test - fun testTokenize_multipleMultiLetterVar_withConsecutiveSingleLetter_producesTokensForAllIds() { - val tokens = MathTokenizer.tokenize("lambda*xyz", ALLOWED_XYZ_WITH_LAMBDA_VARIABLES).toList() - - // The 'lambda' is a single variable, but the individual 'xyz' are separate variables that each - // show up separately (which allows interpreting implicit multiplication). - assertThat(tokens).hasSize(5) - assertThat(tokens[0]).isInstanceOf(Identifier::class.java) - assertThat(tokens[1]).isInstanceOf(Operator::class.java) - assertThat(tokens[2]).isInstanceOf(Identifier::class.java) - assertThat(tokens[3]).isInstanceOf(Identifier::class.java) - assertThat(tokens[4]).isInstanceOf(Identifier::class.java) - assertThat((tokens[0] as Identifier).name).isEqualTo("lambda") - assertThat((tokens[1] as Operator).operator).isEqualTo('*') - assertThat((tokens[2] as Identifier).name).isEqualTo("x") - assertThat((tokens[3] as Identifier).name).isEqualTo("y") - assertThat((tokens[4] as Identifier).name).isEqualTo("z") - } - - @Test - fun testTokenize_ambiguousMutliSingleLetterIds_producesIdForPreferredMultiLetterId() { - val tokens = MathTokenizer.tokenize("xyz", ALLOWED_XYZ_WITH_COMBINED_XYZ_VARIABLES).toList() - - // A single identifier should be parsed since the combined variable is encountered, and that - // takes precedent. - assertThat(tokens).hasSize(1) - assertThat(tokens[0]).isInstanceOf(Identifier::class.java) - assertThat((tokens[0] as Identifier).name).isEqualTo("xyz") - } - - @Test - fun testTokenize_ambiguousMutliSingleLetterIds_singleLetterIdsAlone_producesIdTokens() { - val tokens = MathTokenizer.tokenize("x y z", ALLOWED_XYZ_WITH_COMBINED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(3) - assertThat(tokens[0]).isInstanceOf(Identifier::class.java) - assertThat(tokens[1]).isInstanceOf(Identifier::class.java) - assertThat(tokens[2]).isInstanceOf(Identifier::class.java) - assertThat((tokens[0] as Identifier).name).isEqualTo("x") - assertThat((tokens[1] as Identifier).name).isEqualTo("y") - assertThat((tokens[2] as Identifier).name).isEqualTo("z") - } - - @Test - fun testTokenize_ambiguousMutliSingleLetterIds_multiIdSubstring_producesIndividualIdTokens() { - val tokens = MathTokenizer.tokenize("yz", ALLOWED_XYZ_WITH_COMBINED_XYZ_VARIABLES).toList() - - // Partial substring of 'xyz' produces separate tokens since the whole token isn't present. - assertThat(tokens).hasSize(2) - assertThat(tokens[0]).isInstanceOf(Identifier::class.java) - assertThat(tokens[1]).isInstanceOf(Identifier::class.java) - assertThat((tokens[0] as Identifier).name).isEqualTo("y") - assertThat((tokens[1] as Identifier).name).isEqualTo("z") - } - - @Test - fun testTokenize_ambiguousMutliSingleLetterIds_outOfOrder_producesIdTokens() { - val tokens = MathTokenizer.tokenize("zyx", ALLOWED_XYZ_WITH_COMBINED_XYZ_VARIABLES).toList() - - // Reversing the tokens doesn't match the overall variable, so return separate variables. - assertThat(tokens).hasSize(3) - assertThat(tokens[0]).isInstanceOf(Identifier::class.java) - assertThat(tokens[1]).isInstanceOf(Identifier::class.java) - assertThat(tokens[2]).isInstanceOf(Identifier::class.java) - assertThat((tokens[0] as Identifier).name).isEqualTo("z") - assertThat((tokens[1] as Identifier).name).isEqualTo("y") - assertThat((tokens[2] as Identifier).name).isEqualTo("x") - } - - @Test - fun testTokenize_ambiguousMutliSingleLetterIds_multiWord_withInvalidLetter_producesInvalidId() { - val tokens = MathTokenizer.tokenize("xyzw", ALLOWED_XYZ_WITH_COMBINED_XYZ_VARIABLES).toList() - - // A single letter is sufficient to lead to an error ID. - assertThat(tokens).hasSize(1) - assertThat(tokens[0]).isInstanceOf(InvalidIdentifier::class.java) - assertThat((tokens[0] as InvalidIdentifier).name).isEqualTo("xyzw") - } - - @Test - fun testTokenize_identifier_whitespaceBefore_isIgnored() { - val tokens = MathTokenizer.tokenize(" \r\t\n x", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(Identifier::class.java) - assertThat((tokens.first() as Identifier).name).isEqualTo("x") - } - - @Test - fun testTokenize_identifier_whitespaceAfter_isIgnored() { - val tokens = MathTokenizer.tokenize("x \r\t\n ", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(Identifier::class.java) - assertThat((tokens.first() as Identifier).name).isEqualTo("x") - } - - @Test - fun testTokenize_identifierAndOperator_whitespaceBetween_isIgnored() { - val tokens = MathTokenizer.tokenize("- \r\t\n x", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(2) - assertThat(tokens[0]).isInstanceOf(Operator::class.java) - assertThat(tokens[1]).isInstanceOf(Identifier::class.java) - assertThat((tokens[0] as Operator).operator).isEqualTo('-') - assertThat((tokens[1] as Identifier).name).isEqualTo("x") - } - - @Test - fun testTokenize_digits_withSpaces_producesMultipleWholeNumberTokens() { - val tokens = MathTokenizer.tokenize("1 23 4", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(3) - assertThat(tokens[0]).isInstanceOf(WholeNumber::class.java) - assertThat(tokens[1]).isInstanceOf(WholeNumber::class.java) - assertThat(tokens[2]).isInstanceOf(WholeNumber::class.java) - assertThat((tokens[0] as WholeNumber).value).isEqualTo(1) - assertThat((tokens[1] as WholeNumber).value).isEqualTo(23) - assertThat((tokens[2] as WholeNumber).value).isEqualTo(4) - } - - @Test - fun testTokenize_complexExpressionWithAllTokenTypes_tokenizesEverythingInOrder() { - val tokens = - MathTokenizer.tokenize("133 + 3.14 * x / (11 - 15) ^ 2 ^ 3", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(15) - - assertThat(tokens[0]).isInstanceOf(WholeNumber::class.java) - assertThat((tokens[0] as WholeNumber).value).isEqualTo(133) - - assertThat(tokens[1]).isInstanceOf(Operator::class.java) - assertThat((tokens[1] as Operator).operator).isEqualTo('+') - - assertThat(tokens[2]).isInstanceOf(DecimalNumber::class.java) - assertThat((tokens[2] as DecimalNumber).value).isWithin(1e-5).of(3.14) - - assertThat(tokens[3]).isInstanceOf(Operator::class.java) - assertThat((tokens[3] as Operator).operator).isEqualTo('*') - - assertThat(tokens[4]).isInstanceOf(Identifier::class.java) - assertThat((tokens[4] as Identifier).name).isEqualTo("x") - - assertThat(tokens[5]).isInstanceOf(Operator::class.java) - assertThat((tokens[5] as Operator).operator).isEqualTo('/') - - assertThat(tokens[6]).isInstanceOf(OpenParenthesis::class.java) - - assertThat(tokens[7]).isInstanceOf(WholeNumber::class.java) - assertThat((tokens[7] as WholeNumber).value).isEqualTo(11) - - assertThat(tokens[8]).isInstanceOf(Operator::class.java) - assertThat((tokens[8] as Operator).operator).isEqualTo('-') - - assertThat(tokens[9]).isInstanceOf(WholeNumber::class.java) - assertThat((tokens[9] as WholeNumber).value).isEqualTo(15) - - assertThat(tokens[10]).isInstanceOf(CloseParenthesis::class.java) - - assertThat(tokens[11]).isInstanceOf(Operator::class.java) - assertThat((tokens[11] as Operator).operator).isEqualTo('^') - - assertThat(tokens[12]).isInstanceOf(WholeNumber::class.java) - assertThat((tokens[12] as WholeNumber).value).isEqualTo(2) - - assertThat(tokens[13]).isInstanceOf(Operator::class.java) - assertThat((tokens[13] as Operator).operator).isEqualTo('^') - - assertThat(tokens[14]).isInstanceOf(WholeNumber::class.java) - assertThat((tokens[14] as WholeNumber).value).isEqualTo(3) - } - - @Test - fun testTokenize_integralSymbol_producesInvalidToken() { - val tokens = MathTokenizer.tokenize("∫", ALLOWED_XYZ_VARIABLES).toList() - - assertThat(tokens).hasSize(1) - assertThat(tokens.first()).isInstanceOf(InvalidToken::class.java) - assertThat((tokens.first() as InvalidToken).token).isEqualTo("∫") - }*/ - private class TokenSubject( metadata: FailureMetadata, private val actual: T @@ -701,6 +173,10 @@ class MathTokenizerTest { actual.asVerifiedType() } + fun isIncompleteFunctionName() { + actual.asVerifiedType() + } + private companion object { private inline fun Token.asVerifiedType(): T { assertThat(this).isInstanceOf(T::class.java) diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt deleted file mode 100644 index bbc67d1d775..00000000000 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ /dev/null @@ -1,7737 +0,0 @@ -package org.oppia.android.util.math - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.BooleanSubject -import com.google.common.truth.DoubleSubject -import com.google.common.truth.FailureMetadata -import com.google.common.truth.IntegerSubject -import com.google.common.truth.StringSubject -import com.google.common.truth.Truth.assertAbout -import com.google.common.truth.Truth.assertThat -import com.google.common.truth.Truth.assertWithMessage -import com.google.common.truth.extensions.proto.LiteProtoSubject -import org.junit.Test -import org.junit.runner.RunWith -import org.oppia.android.app.model.Fraction -import org.oppia.android.app.model.MathBinaryOperation -import org.oppia.android.app.model.MathExpression -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION -import org.oppia.android.app.model.MathFunctionCall -import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT -import org.oppia.android.app.model.MathUnaryOperation -import org.oppia.android.app.model.Real -import org.robolectric.annotation.LooperMode -import kotlin.math.sqrt -import org.oppia.android.app.model.ComparableOperationList -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.PRODUCT -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase -import org.oppia.android.app.model.MathEquation -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE -import org.oppia.android.app.model.OppiaLanguage -import org.oppia.android.app.model.OppiaLanguage.ARABIC -import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE -import org.oppia.android.app.model.OppiaLanguage.ENGLISH -import org.oppia.android.app.model.OppiaLanguage.HINDI -import org.oppia.android.app.model.OppiaLanguage.HINGLISH -import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED -import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE -import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED -import org.oppia.android.app.model.Polynomial -import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError -import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError -import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError -import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError -import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError -import org.oppia.android.util.math.MathParsingError.FunctionNameIncompleteError -import org.oppia.android.util.math.MathParsingError.HangingSquareRootError -import org.oppia.android.util.math.MathParsingError.InvalidFunctionInUseError -import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError -import org.oppia.android.util.math.MathParsingError.NestedExponentsError -import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberAfterBinaryOperatorError -import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberBeforeBinaryOperatorError -import org.oppia.android.util.math.MathParsingError.NumberAfterVariableError -import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError -import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError -import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError -import org.oppia.android.util.math.MathParsingError.SubsequentBinaryOperatorsError -import org.oppia.android.util.math.MathParsingError.SubsequentUnaryOperatorsError -import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError -import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError -import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError -import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError -import org.oppia.android.util.math.NumericExpressionParser.Companion.ErrorCheckingMode -import org.oppia.android.util.math.NumericExpressionParser.Companion.MathParsingResult - -/** Tests for [MathExpressionParser]. */ -@RunWith(AndroidJUnit4::class) -@LooperMode(LooperMode.Mode.PAUSED) -class NumericExpressionParserTest { - @Test - fun testErrorCases() { - val failure1 = expectFailureWhenParsingNumericExpression("73 2") - assertThat(failure1).isEqualTo(SpacesBetweenNumbersError) - - val failure2 = expectFailureWhenParsingNumericExpression("(73") - assertThat(failure2).isEqualTo(UnbalancedParenthesesError) - - val failure3 = expectFailureWhenParsingNumericExpression("73)") - assertThat(failure3).isEqualTo(UnbalancedParenthesesError) - - val failure4 = expectFailureWhenParsingNumericExpression("((73)") - assertThat(failure4).isEqualTo(UnbalancedParenthesesError) - - val failure5 = expectFailureWhenParsingNumericExpression("73 (") - assertThat(failure5).isEqualTo(UnbalancedParenthesesError) - - val failure6 = expectFailureWhenParsingNumericExpression("73 )") - assertThat(failure6).isEqualTo(UnbalancedParenthesesError) - - val failure7 = expectFailureWhenParsingNumericExpression("sqrt(73") - assertThat(failure7).isEqualTo(UnbalancedParenthesesError) - - // TODO: test properties on errors (& add better testing library for errors, or at least helpers). - val failure8 = expectFailureWhenParsingNumericExpression("(7 * 2 + 4)") - assertThat(failure8).isInstanceOf(SingleRedundantParenthesesError::class.java) - - val failure9 = expectFailureWhenParsingNumericExpression("((5 + 4))") - assertThat(failure9).isInstanceOf(MultipleRedundantParenthesesError::class.java) - - val failure13 = expectFailureWhenParsingNumericExpression("(((5 + 4)))") - assertThat(failure13).isInstanceOf(MultipleRedundantParenthesesError::class.java) - - val failure14 = expectFailureWhenParsingNumericExpression("1+((5 + 4))") - assertThat(failure14).isInstanceOf(MultipleRedundantParenthesesError::class.java) - - val failure15 = expectFailureWhenParsingNumericExpression("1+(7*((( 9 + 3) )))") - assertThat(failure15).isInstanceOf(MultipleRedundantParenthesesError::class.java) - assertThat((failure15 as MultipleRedundantParenthesesError).rawExpression) - .isEqualTo("(( 9 + 3) )") - - parseNumericExpressionWithAllErrors("1+(5+4)") - parseNumericExpressionWithAllErrors("(5+4)+1") - - val failure10 = expectFailureWhenParsingNumericExpression("(5) + 4") - assertThat(failure10).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) - - val failure11 = expectFailureWhenParsingNumericExpression("5^(2)") - assertThat(failure11).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) - assertThat((failure11 as RedundantParenthesesForIndividualTermsError).rawExpression) - .isEqualTo("2") - - val failure12 = expectFailureWhenParsingNumericExpression("sqrt((2))") - assertThat(failure12).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) - - val failure16 = expectFailureWhenParsingNumericExpression("$2") - assertThat(failure16).isInstanceOf(UnnecessarySymbolsError::class.java) - assertThat((failure16 as UnnecessarySymbolsError).invalidSymbol).isEqualTo("$") - - val failure17 = expectFailureWhenParsingNumericExpression("5%") - assertThat(failure17).isInstanceOf(UnnecessarySymbolsError::class.java) - assertThat((failure17 as UnnecessarySymbolsError).invalidSymbol).isEqualTo("%") - - val failure18 = expectFailureWhenParsingAlgebraicExpression("x5") - assertThat(failure18).isInstanceOf(NumberAfterVariableError::class.java) - assertThat((failure18 as NumberAfterVariableError).number.integer).isEqualTo(5) - assertThat(failure18.variable).isEqualTo("x") - - val failure19 = expectFailureWhenParsingAlgebraicExpression("2+y 3.14*7") - assertThat(failure19).isInstanceOf(NumberAfterVariableError::class.java) - assertThat((failure19 as NumberAfterVariableError).number.irrational).isWithin(1e-5).of(3.14) - assertThat(failure19.variable).isEqualTo("y") - - // TODO: expand to multiple tests or use parametrized tests. - // RHS operators don't result in unary operations (which are valid in the grammar). - val rhsOperators = listOf("*", "×", "/", "÷", "^") - val lhsOperators = rhsOperators + listOf("+", "-", "−") - val operatorCombinations = lhsOperators.flatMap { op1 -> rhsOperators.map { op1 to it } } - for ((op1, op2) in operatorCombinations) { - val failure22 = expectFailureWhenParsingNumericExpression(expression = "1 $op1$op2 2") - assertThat(failure22).isInstanceOf(SubsequentBinaryOperatorsError::class.java) - assertThat((failure22 as SubsequentBinaryOperatorsError).operator1).isEqualTo(op1) - assertThat(failure22.operator2).isEqualTo(op2) - } - - val failure37 = expectFailureWhenParsingNumericExpression("++2") - assertThat(failure37).isInstanceOf(SubsequentUnaryOperatorsError::class.java) - - val failure38 = expectFailureWhenParsingAlgebraicExpression("--x") - assertThat(failure38).isInstanceOf(SubsequentUnaryOperatorsError::class.java) - - val failure39 = expectFailureWhenParsingAlgebraicExpression("-+x") - assertThat(failure39).isInstanceOf(SubsequentUnaryOperatorsError::class.java) - - val failure40 = expectFailureWhenParsingNumericExpression("+-2") - assertThat(failure40).isInstanceOf(SubsequentUnaryOperatorsError::class.java) - - parseNumericExpressionWithAllErrors("2++3") // Will succeed since it's 2 + (+2). - val failure41 = expectFailureWhenParsingNumericExpression("2+++3") - assertThat(failure41).isInstanceOf(SubsequentUnaryOperatorsError::class.java) - - val failure23 = expectFailureWhenParsingNumericExpression("/2") - assertThat(failure23).isInstanceOf(NoVariableOrNumberBeforeBinaryOperatorError::class.java) - assertThat((failure23 as NoVariableOrNumberBeforeBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.DIVIDE) - - val failure24 = expectFailureWhenParsingAlgebraicExpression("*x") - assertThat(failure24).isInstanceOf(NoVariableOrNumberBeforeBinaryOperatorError::class.java) - assertThat((failure24 as NoVariableOrNumberBeforeBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.MULTIPLY) - - val failure27 = expectFailureWhenParsingNumericExpression("2^") - assertThat(failure27).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) - assertThat((failure27 as NoVariableOrNumberAfterBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.EXPONENTIATE) - - val failure25 = expectFailureWhenParsingNumericExpression("2/") - assertThat(failure25).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) - assertThat((failure25 as NoVariableOrNumberAfterBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.DIVIDE) - - val failure26 = expectFailureWhenParsingAlgebraicExpression("x*") - assertThat(failure26).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) - assertThat((failure26 as NoVariableOrNumberAfterBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.MULTIPLY) - - val failure28 = expectFailureWhenParsingAlgebraicExpression("x+") - assertThat(failure28).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) - assertThat((failure28 as NoVariableOrNumberAfterBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.ADD) - - val failure29 = expectFailureWhenParsingAlgebraicExpression("x-") - assertThat(failure29).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) - assertThat((failure29 as NoVariableOrNumberAfterBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.SUBTRACT) - - val failure42 = expectFailureWhenParsingAlgebraicExpression("2^x") - assertThat(failure42).isInstanceOf(ExponentIsVariableExpressionError::class.java) - - val failure43 = expectFailureWhenParsingAlgebraicExpression("2^(1+x)") - assertThat(failure43).isInstanceOf(ExponentIsVariableExpressionError::class.java) - - val failure44 = expectFailureWhenParsingAlgebraicExpression("2^3^x") - assertThat(failure44).isInstanceOf(ExponentIsVariableExpressionError::class.java) - - val failure45 = expectFailureWhenParsingAlgebraicExpression("2^sqrt(x)") - assertThat(failure45).isInstanceOf(ExponentIsVariableExpressionError::class.java) - - val failure46 = expectFailureWhenParsingNumericExpression("2^7") - assertThat(failure46).isInstanceOf(ExponentTooLargeError::class.java) - - val failure47 = expectFailureWhenParsingNumericExpression("2^30.12") - assertThat(failure47).isInstanceOf(ExponentTooLargeError::class.java) - - parseNumericExpressionWithAllErrors("2^3") - - val failure48 = expectFailureWhenParsingNumericExpression("2^3^2") - assertThat(failure48).isInstanceOf(NestedExponentsError::class.java) - - val failure49 = expectFailureWhenParsingAlgebraicExpression("x^2^5") - assertThat(failure49).isInstanceOf(NestedExponentsError::class.java) - - val failure20 = expectFailureWhenParsingNumericExpression("2√") - assertThat(failure20).isInstanceOf(HangingSquareRootError::class.java) - - val failure50 = expectFailureWhenParsingNumericExpression("2/0") - assertThat(failure50).isInstanceOf(TermDividedByZeroError::class.java) - - val failure51 = expectFailureWhenParsingAlgebraicExpression("x/0") - assertThat(failure51).isInstanceOf(TermDividedByZeroError::class.java) - - val failure52 = expectFailureWhenParsingNumericExpression("sqrt(2+7/0.0)") - assertThat(failure52).isInstanceOf(TermDividedByZeroError::class.java) - - val failure21 = expectFailureWhenParsingNumericExpression("x+y") - assertThat(failure21).isInstanceOf(VariableInNumericExpressionError::class.java) - - val failure53 = expectFailureWhenParsingAlgebraicExpression("x+y+a") - assertThat(failure53).isInstanceOf(DisabledVariablesInUseError::class.java) - assertThat((failure53 as DisabledVariablesInUseError).variables).containsExactly("a") - - val failure54 = expectFailureWhenParsingAlgebraicExpression("apple") - assertThat(failure54).isInstanceOf(DisabledVariablesInUseError::class.java) - assertThat((failure54 as DisabledVariablesInUseError).variables) - .containsExactly("a", "p", "l", "e") - - val failure55 = - expectFailureWhenParsingAlgebraicExpression("apple", allowedVariables = listOf("a", "p", "l")) - assertThat(failure55).isInstanceOf(DisabledVariablesInUseError::class.java) - assertThat((failure55 as DisabledVariablesInUseError).variables).containsExactly("e") - - parseAlgebraicExpressionWithAllErrors("x+y+z") - - val failure56 = - expectFailureWhenParsingAlgebraicExpression("x+y+z", allowedVariables = listOf()) - assertThat(failure56).isInstanceOf(DisabledVariablesInUseError::class.java) - assertThat((failure56 as DisabledVariablesInUseError).variables).containsExactly("x", "y", "z") - - val failure30 = expectFailureWhenParsingAlgebraicEquation("x==2") - assertThat(failure30).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) - - val failure31 = expectFailureWhenParsingAlgebraicEquation("x=2=y") - assertThat(failure31).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) - - val failure32 = expectFailureWhenParsingAlgebraicEquation("x=2=") - assertThat(failure32).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) - - val failure33 = expectFailureWhenParsingAlgebraicEquation("x=") - assertThat(failure33).isInstanceOf(EquationMissingLhsOrRhsError::class.java) - - val failure34 = expectFailureWhenParsingAlgebraicEquation("=x") - assertThat(failure34).isInstanceOf(EquationMissingLhsOrRhsError::class.java) - - val failure35 = expectFailureWhenParsingAlgebraicEquation("=x") - assertThat(failure35).isInstanceOf(EquationMissingLhsOrRhsError::class.java) - - // TODO: expand to multiple tests or use parametrized tests. - val prohibitedFunctionNames = - listOf( - "exp", "log", "log10", "ln", "sin", "cos", "tan", "cot", "csc", "sec", "atan", "asin", - "acos", "abs" - ) - for (functionName in prohibitedFunctionNames) { - val failure36 = expectFailureWhenParsingAlgebraicEquation("$functionName(0.5)") - assertThat(failure36).isInstanceOf(InvalidFunctionInUseError::class.java) - assertThat((failure36 as InvalidFunctionInUseError).functionName).isEqualTo(functionName) - } - - val failure57 = expectFailureWhenParsingAlgebraicExpression("sq") - assertThat(failure57).isInstanceOf(FunctionNameIncompleteError::class.java) - - val failure58 = expectFailureWhenParsingAlgebraicExpression("sqr") - assertThat(failure58).isInstanceOf(FunctionNameIncompleteError::class.java) - - // TODO: Other cases: sqrt(, sqrt(), sqrt 2, +2 - } - - @Test - fun testLotsOfCasesForNumericExpression() { - // TODO: split this up - // TODO: add log string generation for expressions. - expectFailureWhenParsingNumericExpression("") - - val expression1 = parseNumericExpressionWithoutOptionalErrors("1") - assertThat(expression1).hasStructureThatMatches { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - assertThat(expression1).evaluatesToIntegerThat().isEqualTo(1) - - expectFailureWhenParsingNumericExpression("x") - - val expression2 = parseNumericExpressionWithoutOptionalErrors(" 2 ") - assertThat(expression2).hasStructureThatMatches { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - assertThat(expression2).evaluatesToIntegerThat().isEqualTo(2) - - val expression3 = parseNumericExpressionWithoutOptionalErrors(" 2.5 ") - assertThat(expression3).hasStructureThatMatches { - constant { - withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) - } - } - assertThat(expression3).evaluatesToIrrationalThat().isWithin(1e-5).of(2.5) - - expectFailureWhenParsingNumericExpression(" x ") - - expectFailureWhenParsingNumericExpression(" z x ") - - val expression4 = parseNumericExpressionWithoutOptionalErrors("2^3^2") - assertThat(expression4).hasStructureThatMatches { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression4).evaluatesToIntegerThat().isEqualTo(512) - - val expression23 = parseNumericExpressionWithoutOptionalErrors("(2^3)^2") - assertThat(expression23).hasStructureThatMatches { - exponentiation { - leftOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - assertThat(expression23).evaluatesToIntegerThat().isEqualTo(64) - - val expression24 = parseNumericExpressionWithoutOptionalErrors("512/32/4") - assertThat(expression24).hasStructureThatMatches { - division { - leftOperand { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(512) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(32) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - assertThat(expression24).evaluatesToIntegerThat().isEqualTo(4) - - val expression25 = parseNumericExpressionWithoutOptionalErrors("512/(32/4)") - assertThat(expression25).hasStructureThatMatches { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(512) - } - } - rightOperand { - group { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(32) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - assertThat(expression25).evaluatesToIntegerThat().isEqualTo(64) - - val expression5 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)") - assertThat(expression5).hasStructureThatMatches { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - assertThat(expression5).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0)) - - expectFailureWhenParsingNumericExpression("sqr(2)") - - expectFailureWhenParsingNumericExpression("xyz(2)") - - val expression6 = parseNumericExpressionWithoutOptionalErrors("732") - assertThat(expression6).hasStructureThatMatches { - constant { - withValueThat().isIntegerThat().isEqualTo(732) - } - } - assertThat(expression6).evaluatesToIntegerThat().isEqualTo(732) - - // Verify order of operations between higher & lower precedent operators. - val expression32 = parseNumericExpressionWithoutOptionalErrors("3+4^7") - assertThat(expression32).hasStructureThatMatches { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - } - } - } - } - assertThat(expression32).evaluatesToIntegerThat().isEqualTo(16387) - - val expression7 = parseNumericExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") - assertThat(expression7).hasStructureThatMatches { - // To better visualize the precedence & order of operations, see this grouped version: - // (((3*2)-3)+((((4^7)*8)/3)*2))+7. - addition { - leftOperand { - // ((3*2)-3)+((((4^7)*8)/3)*2) - addition { - leftOperand { - // (1*2)-3 - subtraction { - leftOperand { - // 3*2 - multiplication { - leftOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - // (((4^7)*8)/3)*2 - multiplication { - leftOperand { - // ((4^7)*8)/3 - division { - leftOperand { - // (4^7)*8 - multiplication { - leftOperand { - // 4^7 - exponentiation { - leftOperand { - // 4 - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - rightOperand { - // 7 - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - } - } - rightOperand { - // 8 - constant { - withValueThat().isIntegerThat().isEqualTo(8) - } - } - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - rightOperand { - // 7 - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - } - } - assertThat(expression7) - .evaluatesToRationalThat() - .evaluatesToRealThat() - .isWithin(1e-5) - .of(87391.333333333) - - expectFailureWhenParsingNumericExpression("x = √2 × 7 ÷ 4") - - val expression8 = parseNumericExpressionWithoutOptionalErrors("(1+2)(3+4)") - assertThat(expression8).hasStructureThatMatches { - multiplication { - leftOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - assertThat(expression8).evaluatesToIntegerThat().isEqualTo(21) - - // Right implicit multiplication of numbers isn't allowed. - expectFailureWhenParsingNumericExpression("(1+2)2") - - val expression10 = parseNumericExpressionWithoutOptionalErrors("2(1+2)") - assertThat(expression10).hasStructureThatMatches { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - assertThat(expression10).evaluatesToIntegerThat().isEqualTo(6) - - // Right implicit multiplication of numbers isn't allowed. - expectFailureWhenParsingNumericExpression("sqrt(2)3") - - val expression12 = parseNumericExpressionWithoutOptionalErrors("3sqrt(2)") - assertThat(expression12).hasStructureThatMatches { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression12).evaluatesToIrrationalThat().isWithin(1e-5).of(3.0 * sqrt(2.0)) - - expectFailureWhenParsingNumericExpression("xsqrt(2)") - - val expression13 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)*(1+2)*(3-2^5)") - assertThat(expression13).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - } - } - } - } - } - assertThat(expression13).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) - - val expression58 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)(1+2)(3-2^5)") - assertThat(expression58).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - } - } - } - } - } - assertThat(expression58).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) - - val expression14 = parseNumericExpressionWithoutOptionalErrors("((3))") - assertThat(expression14).hasStructureThatMatches { - group { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - assertThat(expression14).evaluatesToIntegerThat().isEqualTo(3) - - val expression15 = parseNumericExpressionWithoutOptionalErrors("++3") - assertThat(expression15).hasStructureThatMatches { - positive { - operand { - positive { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - assertThat(expression15).evaluatesToIntegerThat().isEqualTo(3) - - val expression16 = parseNumericExpressionWithoutOptionalErrors("--4") - assertThat(expression16).hasStructureThatMatches { - negation { - operand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - assertThat(expression16).evaluatesToIntegerThat().isEqualTo(4) - - val expression17 = parseNumericExpressionWithoutOptionalErrors("1+-4") - assertThat(expression17).hasStructureThatMatches { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - assertThat(expression17).evaluatesToIntegerThat().isEqualTo(-3) - - val expression18 = parseNumericExpressionWithoutOptionalErrors("1++4") - assertThat(expression18).hasStructureThatMatches { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - positive { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - assertThat(expression18).evaluatesToIntegerThat().isEqualTo(5) - - val expression19 = parseNumericExpressionWithoutOptionalErrors("1--4") - assertThat(expression19).hasStructureThatMatches { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - assertThat(expression19).evaluatesToIntegerThat().isEqualTo(5) - - expectFailureWhenParsingNumericExpression("1-^-4") - - val expression20 = parseNumericExpressionWithoutOptionalErrors("√2 × 7 ÷ 4") - assertThat(expression20).hasStructureThatMatches { - division { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - assertThat(expression20).evaluatesToIrrationalThat().isWithin(1e-5).of((sqrt(2.0) * 7.0) / 4.0) - - expectFailureWhenParsingNumericExpression("1+2 &asdf") - - val expression21 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)sqrt(3)sqrt(4)") - // Note that this tree demonstrates left associativity. - assertThat(expression21).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - assertThat(expression21) - .evaluatesToIrrationalThat() - .isWithin(1e-5) - .of(sqrt(2.0) * sqrt(3.0) * sqrt(4.0)) - - val expression22 = parseNumericExpressionWithoutOptionalErrors("(1+2)(3-7^2)(5+-17)") - assertThat(expression22).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - // 1+2 - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - rightOperand { - // 3-7^2 - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - } - rightOperand { - // 5+-17 - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(17) - } - } - } - } - } - } - } - } - } - assertThat(expression22).evaluatesToIntegerThat().isEqualTo(1656) - - val expression26 = parseNumericExpressionWithoutOptionalErrors("3^-2") - assertThat(expression26).hasStructureThatMatches { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression26).evaluatesToRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(9) - } - - val expression27 = parseNumericExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") - assertThat(expression27).hasStructureThatMatches { - exponentiation { - leftOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - rightOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - } - assertThat(expression27).evaluatesToIrrationalThat().isWithin(1e-5).of(0.78338103693) - - val expression28 = parseNumericExpressionWithoutOptionalErrors("1-3^sqrt(4)") - assertThat(expression28).hasStructureThatMatches { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - } - assertThat(expression28).evaluatesToIntegerThat().isEqualTo(-8) - - // "Hard" order of operation problems loosely based on & other problems that can often stump - // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. - val expression29 = parseNumericExpressionWithoutOptionalErrors("3÷2*(3+4)") - assertThat(expression29).hasStructureThatMatches { - multiplication { - leftOperand { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - assertThat(expression29).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) - - val expression59 = parseNumericExpressionWithoutOptionalErrors("3÷2(3+4)") - assertThat(expression59).hasStructureThatMatches { - multiplication { - leftOperand { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - assertThat(expression59).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) - - // Numbers cannot have implicit multiplication unless they are in groups. - expectFailureWhenParsingNumericExpression("2 2") - - expectFailureWhenParsingNumericExpression("2 2^2") - - expectFailureWhenParsingNumericExpression("2^2 2") - - val expression31 = parseNumericExpressionWithoutOptionalErrors("(3)(4)(5)") - assertThat(expression31).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - } - assertThat(expression31).evaluatesToIntegerThat().isEqualTo(60) - - val expression33 = parseNumericExpressionWithoutOptionalErrors("2^(3)") - assertThat(expression33).hasStructureThatMatches { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - assertThat(expression33).evaluatesToIntegerThat().isEqualTo(8) - - // Verify that implicit multiple has lower precedence than exponentiation. - val expression34 = parseNumericExpressionWithoutOptionalErrors("2^(3)(4)") - assertThat(expression34).hasStructureThatMatches { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - assertThat(expression34).evaluatesToIntegerThat().isEqualTo(32) - - // An exponentiation can never be an implicit right operand. - expectFailureWhenParsingNumericExpression("2^(3)2^2") - - val expression35 = parseNumericExpressionWithoutOptionalErrors("2^(3)*2^2") - assertThat(expression35).hasStructureThatMatches { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression35).evaluatesToIntegerThat().isEqualTo(32) - - // An exponentiation can be a right operand of an implicit mult if it's grouped. - val expression36 = parseNumericExpressionWithoutOptionalErrors("2^(3)(2^2)") - assertThat(expression36).hasStructureThatMatches { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - rightOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - assertThat(expression36).evaluatesToIntegerThat().isEqualTo(32) - - // An exponentiation can never be an implicit right operand. - expectFailureWhenParsingNumericExpression("2^3(4)2^3") - - val expression38 = parseNumericExpressionWithoutOptionalErrors("2^3(4)*2^3") - assertThat(expression38).hasStructureThatMatches { - // 2^3(4)*2^3 - multiplication { - leftOperand { - // 2^3(4) - multiplication { - leftOperand { - // 2^3 - exponentiation { - leftOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - // 4 - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - rightOperand { - // 2^3 - exponentiation { - leftOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - assertThat(expression38).evaluatesToIntegerThat().isEqualTo(256) - - expectFailureWhenParsingNumericExpression("2^2 2^2") - expectFailureWhenParsingNumericExpression("(3) 2^2") - expectFailureWhenParsingNumericExpression("sqrt(3) 2^2") - expectFailureWhenParsingNumericExpression("√2 2^2") - expectFailureWhenParsingNumericExpression("2^2 3") - - expectFailureWhenParsingNumericExpression("-2 3") - - val expression39 = parseNumericExpressionWithoutOptionalErrors("-(1+2)") - assertThat(expression39).hasStructureThatMatches { - negation { - operand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - assertThat(expression39).evaluatesToIntegerThat().isEqualTo(-3) - - // Should pass for algebra. - expectFailureWhenParsingNumericExpression("-2 x") - - val expression40 = parseNumericExpressionWithoutOptionalErrors("-2 (1+2)") - assertThat(expression40).hasStructureThatMatches { - // The negation happens last for parity with other common calculators. - negation { - operand { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - } - assertThat(expression40).evaluatesToIntegerThat().isEqualTo(-6) - - val expression41 = parseNumericExpressionWithoutOptionalErrors("-2^3(4)") - assertThat(expression41).hasStructureThatMatches { - negation { - operand { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - assertThat(expression41).evaluatesToIntegerThat().isEqualTo(-32) - - val expression43 = parseNumericExpressionWithoutOptionalErrors("√2^2(3)") - assertThat(expression43).hasStructureThatMatches { - multiplication { - leftOperand { - exponentiation { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - assertThat(expression43).evaluatesToIrrationalThat().isWithin(1e-5).of(6.0) - - val expression60 = parseNumericExpressionWithoutOptionalErrors("√(2^2(3))") - assertThat(expression60).hasStructureThatMatches { - functionCallTo(SQUARE_ROOT) { - argument { - group { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - } - } - assertThat(expression60).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(12.0)) - - val expression42 = parseNumericExpressionWithoutOptionalErrors("-2*-2") - // Note that the following structure is not the same as (-2)*(-2) since unary negation has - // higher precedence than multiplication, so it's first & recurses to include the entire - // multiplication expression. - assertThat(expression42).hasStructureThatMatches { - negation { - operand { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - assertThat(expression42).evaluatesToIntegerThat().isEqualTo(4) - - val expression44 = parseNumericExpressionWithoutOptionalErrors("2(2)") - assertThat(expression44).hasStructureThatMatches { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - assertThat(expression44).evaluatesToIntegerThat().isEqualTo(4) - - val expression45 = parseNumericExpressionWithoutOptionalErrors("2sqrt(2)") - assertThat(expression45).hasStructureThatMatches { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression45).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) - - val expression46 = parseNumericExpressionWithoutOptionalErrors("2√2") - assertThat(expression46).hasStructureThatMatches { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression46).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) - - val expression47 = parseNumericExpressionWithoutOptionalErrors("(2)(2)") - assertThat(expression47).hasStructureThatMatches { - multiplication { - leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - assertThat(expression47).evaluatesToIntegerThat().isEqualTo(4) - - val expression48 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)(2)") - assertThat(expression48).hasStructureThatMatches { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - assertThat(expression48).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) - - val expression49 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)sqrt(2)") - assertThat(expression49).hasStructureThatMatches { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression49).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) - - val expression50 = parseNumericExpressionWithoutOptionalErrors("√2√2") - assertThat(expression50).hasStructureThatMatches { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression50).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) - - val expression51 = parseNumericExpressionWithoutOptionalErrors("(2)(2)(2)") - assertThat(expression51).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - assertThat(expression51).evaluatesToIntegerThat().isEqualTo(8) - - val expression52 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)sqrt(2)sqrt(2)") - assertThat(expression52).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - val sqrt2 = sqrt(2.0) - assertThat(expression52).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) - - val expression53 = parseNumericExpressionWithoutOptionalErrors("√2√2√2") - assertThat(expression53).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression53).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) - - // Should fail for algebra. - expectFailureWhenParsingNumericExpression("x7") - - // Should pass for algebra. - expectFailureWhenParsingNumericExpression("2x^2") - - val expression54 = parseNumericExpressionWithoutOptionalErrors("2*2/-4+7*2") - assertThat(expression54).hasStructureThatMatches { - // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) - addition { - leftOperand { - // 2*2/-4 - division { - leftOperand { - // 2*2 - multiplication { - leftOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - // -4 - negation { - // 4 - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - rightOperand { - // 7*2 - multiplication { - leftOperand { - // 7 - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression54).evaluatesToIntegerThat().isEqualTo(13) - - val expression55 = parseNumericExpressionWithoutOptionalErrors("3/(1-2)") - assertThat(expression55).hasStructureThatMatches { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - assertThat(expression55).evaluatesToIntegerThat().isEqualTo(-3) - - val expression56 = parseNumericExpressionWithoutOptionalErrors("(3)/(1-2)") - assertThat(expression56).hasStructureThatMatches { - division { - leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - assertThat(expression56).evaluatesToIntegerThat().isEqualTo(-3) - - val expression57 = parseNumericExpressionWithoutOptionalErrors("3/((1-2))") - assertThat(expression57).hasStructureThatMatches { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - group { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - assertThat(expression57).evaluatesToIntegerThat().isEqualTo(-3) - - // TODO: add others, including tests for malformed expressions throughout the parser & - // tokenizer. - } - - @Test - fun testLotsOfCasesForAlgebraicExpression() { - // TODO: split this up - // TODO: add log string generation for expressions. - expectFailureWhenParsingAlgebraicExpression("") - - val expression1 = parseAlgebraicExpressionWithoutOptionalErrors("1") - assertThat(expression1).hasStructureThatMatches { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - assertThat(expression1).evaluatesToIntegerThat().isEqualTo(1) - - val expression61 = parseAlgebraicExpressionWithoutOptionalErrors("x") - assertThat(expression61).hasStructureThatMatches { - variable { - withNameThat().isEqualTo("x") - } - } - - val expression2 = parseAlgebraicExpressionWithoutOptionalErrors(" 2 ") - assertThat(expression2).hasStructureThatMatches { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - assertThat(expression2).evaluatesToIntegerThat().isEqualTo(2) - - val expression3 = parseAlgebraicExpressionWithoutOptionalErrors(" 2.5 ") - assertThat(expression3).hasStructureThatMatches { - constant { - withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) - } - } - assertThat(expression3).evaluatesToIrrationalThat().isWithin(1e-5).of(2.5) - - val expression62 = parseAlgebraicExpressionWithoutOptionalErrors(" y ") - assertThat(expression62).hasStructureThatMatches { - variable { - withNameThat().isEqualTo("y") - } - } - - val expression63 = parseAlgebraicExpressionWithoutOptionalErrors(" z x ") - assertThat(expression63).hasStructureThatMatches { - multiplication { - leftOperand { - variable { - withNameThat().isEqualTo("z") - } - } - rightOperand { - variable { - withNameThat().isEqualTo("x") - } - } - } - } - - val expression4 = parseAlgebraicExpressionWithoutOptionalErrors("2^3^2") - assertThat(expression4).hasStructureThatMatches { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression4).evaluatesToIntegerThat().isEqualTo(512) - - val expression23 = parseAlgebraicExpressionWithoutOptionalErrors("(2^3)^2") - assertThat(expression23).hasStructureThatMatches { - exponentiation { - leftOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - assertThat(expression23).evaluatesToIntegerThat().isEqualTo(64) - - val expression24 = parseAlgebraicExpressionWithoutOptionalErrors("512/32/4") - assertThat(expression24).hasStructureThatMatches { - division { - leftOperand { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(512) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(32) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - assertThat(expression24).evaluatesToIntegerThat().isEqualTo(4) - - val expression25 = parseAlgebraicExpressionWithoutOptionalErrors("512/(32/4)") - assertThat(expression25).hasStructureThatMatches { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(512) - } - } - rightOperand { - group { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(32) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - assertThat(expression25).evaluatesToIntegerThat().isEqualTo(64) - - val expression5 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)") - assertThat(expression5).hasStructureThatMatches { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - assertThat(expression5).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0)) - - expectFailureWhenParsingAlgebraicExpression("sqr(2)") - - val expression64 = parseAlgebraicExpressionWithoutOptionalErrors("xyz(2)") - assertThat(expression64).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - multiplication { - leftOperand { - variable { - withNameThat().isEqualTo("x") - } - } - rightOperand { - variable { - withNameThat().isEqualTo("y") - } - } - } - } - rightOperand { - variable { - withNameThat().isEqualTo("z") - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - - val expression6 = parseAlgebraicExpressionWithoutOptionalErrors("732") - assertThat(expression6).hasStructureThatMatches { - constant { - withValueThat().isIntegerThat().isEqualTo(732) - } - } - assertThat(expression6).evaluatesToIntegerThat().isEqualTo(732) - - expectFailureWhenParsingAlgebraicExpression("73 2") - - // Verify order of operations between higher & lower precedent operators. - val expression32 = parseAlgebraicExpressionWithoutOptionalErrors("3+4^7") - assertThat(expression32).hasStructureThatMatches { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - } - } - } - } - assertThat(expression32).evaluatesToIntegerThat().isEqualTo(16387) - - val expression7 = parseAlgebraicExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") - assertThat(expression7).hasStructureThatMatches { - // To better visualize the precedence & order of operations, see this grouped version: - // (((3*2)-3)+((((4^7)*8)/3)*2))+7. - addition { - leftOperand { - // ((3*2)-3)+((((4^7)*8)/3)*2) - addition { - leftOperand { - // (1*2)-3 - subtraction { - leftOperand { - // 3*2 - multiplication { - leftOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - // (((4^7)*8)/3)*2 - multiplication { - leftOperand { - // ((4^7)*8)/3 - division { - leftOperand { - // (4^7)*8 - multiplication { - leftOperand { - // 4^7 - exponentiation { - leftOperand { - // 4 - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - rightOperand { - // 7 - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - } - } - rightOperand { - // 8 - constant { - withValueThat().isIntegerThat().isEqualTo(8) - } - } - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - rightOperand { - // 7 - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - } - } - assertThat(expression7) - .evaluatesToRationalThat() - .evaluatesToRealThat() - .isWithin(1e-5) - .of(87391.333333333) - - expectFailureWhenParsingAlgebraicExpression("x = √2 × 7 ÷ 4") - - val expression8 = parseAlgebraicExpressionWithoutOptionalErrors("(1+2)(3+4)") - assertThat(expression8).hasStructureThatMatches { - multiplication { - leftOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - assertThat(expression8).evaluatesToIntegerThat().isEqualTo(21) - - // Right implicit multiplication of numbers isn't allowed. - expectFailureWhenParsingAlgebraicExpression("(1+2)2") - - val expression10 = parseAlgebraicExpressionWithoutOptionalErrors("2(1+2)") - assertThat(expression10).hasStructureThatMatches { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - assertThat(expression10).evaluatesToIntegerThat().isEqualTo(6) - - // Right implicit multiplication of numbers isn't allowed. - expectFailureWhenParsingAlgebraicExpression("sqrt(2)3") - - val expression12 = parseAlgebraicExpressionWithoutOptionalErrors("3sqrt(2)") - assertThat(expression12).hasStructureThatMatches { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression12).evaluatesToIrrationalThat().isWithin(1e-5).of(3.0 * sqrt(2.0)) - - val expression65 = parseAlgebraicExpressionWithoutOptionalErrors("xsqrt(2)") - assertThat(expression65).hasStructureThatMatches { - multiplication { - leftOperand { - variable { - withNameThat().isEqualTo("x") - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - - val expression13 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)*(1+2)*(3-2^5)") - assertThat(expression13).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - } - } - } - } - } - assertThat(expression13).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) - - val expression58 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)(1+2)(3-2^5)") - assertThat(expression58).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - } - } - } - } - } - assertThat(expression58).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) - - val expression14 = parseAlgebraicExpressionWithoutOptionalErrors("((3))") - assertThat(expression14).hasStructureThatMatches { - group { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - assertThat(expression14).evaluatesToIntegerThat().isEqualTo(3) - - val expression15 = parseAlgebraicExpressionWithoutOptionalErrors("++3") - assertThat(expression15).hasStructureThatMatches { - positive { - operand { - positive { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - assertThat(expression15).evaluatesToIntegerThat().isEqualTo(3) - - val expression16 = parseAlgebraicExpressionWithoutOptionalErrors("--4") - assertThat(expression16).hasStructureThatMatches { - negation { - operand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - assertThat(expression16).evaluatesToIntegerThat().isEqualTo(4) - - val expression17 = parseAlgebraicExpressionWithoutOptionalErrors("1+-4") - assertThat(expression17).hasStructureThatMatches { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - assertThat(expression17).evaluatesToIntegerThat().isEqualTo(-3) - - val expression18 = parseAlgebraicExpressionWithoutOptionalErrors("1++4") - assertThat(expression18).hasStructureThatMatches { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - positive { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - assertThat(expression18).evaluatesToIntegerThat().isEqualTo(5) - - val expression19 = parseAlgebraicExpressionWithoutOptionalErrors("1--4") - assertThat(expression19).hasStructureThatMatches { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - assertThat(expression19).evaluatesToIntegerThat().isEqualTo(5) - - expectFailureWhenParsingAlgebraicExpression("1-^-4") - - val expression20 = parseAlgebraicExpressionWithoutOptionalErrors("√2 × 7 ÷ 4") - assertThat(expression20).hasStructureThatMatches { - division { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - assertThat(expression20).evaluatesToIrrationalThat().isWithin(1e-5).of((sqrt(2.0) * 7.0) / 4.0) - - expectFailureWhenParsingAlgebraicExpression("1+2 &asdf") - - val expression21 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)sqrt(3)sqrt(4)") - // Note that this tree demonstrates left associativity. - assertThat(expression21).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - assertThat(expression21) - .evaluatesToIrrationalThat() - .isWithin(1e-5) - .of(sqrt(2.0) * sqrt(3.0) * sqrt(4.0)) - - val expression22 = parseAlgebraicExpressionWithoutOptionalErrors("(1+2)(3-7^2)(5+-17)") - assertThat(expression22).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - // 1+2 - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - rightOperand { - // 3-7^2 - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - } - rightOperand { - // 5+-17 - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(17) - } - } - } - } - } - } - } - } - } - assertThat(expression22).evaluatesToIntegerThat().isEqualTo(1656) - - val expression26 = parseAlgebraicExpressionWithoutOptionalErrors("3^-2") - assertThat(expression26).hasStructureThatMatches { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression26).evaluatesToRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(9) - } - - val expression27 = parseAlgebraicExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") - assertThat(expression27).hasStructureThatMatches { - exponentiation { - leftOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - rightOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - } - assertThat(expression27).evaluatesToIrrationalThat().isWithin(1e-5).of(0.78338103693) - - val expression28 = parseAlgebraicExpressionWithoutOptionalErrors("1-3^sqrt(4)") - assertThat(expression28).hasStructureThatMatches { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - } - assertThat(expression28).evaluatesToIntegerThat().isEqualTo(-8) - - // "Hard" order of operation problems loosely based on & other problems that can often stump - // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. - val expression29 = parseAlgebraicExpressionWithoutOptionalErrors("3÷2*(3+4)") - assertThat(expression29).hasStructureThatMatches { - multiplication { - leftOperand { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - assertThat(expression29).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) - - val expression59 = parseAlgebraicExpressionWithoutOptionalErrors("3÷2(3+4)") - assertThat(expression59).hasStructureThatMatches { - multiplication { - leftOperand { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - assertThat(expression59).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) - - // Numbers cannot have implicit multiplication unless they are in groups. - expectFailureWhenParsingAlgebraicExpression("2 2") - - expectFailureWhenParsingAlgebraicExpression("2 2^2") - - expectFailureWhenParsingAlgebraicExpression("2^2 2") - - val expression31 = parseAlgebraicExpressionWithoutOptionalErrors("(3)(4)(5)") - assertThat(expression31).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - } - assertThat(expression31).evaluatesToIntegerThat().isEqualTo(60) - - val expression33 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)") - assertThat(expression33).hasStructureThatMatches { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - assertThat(expression33).evaluatesToIntegerThat().isEqualTo(8) - - // Verify that implicit multiple has lower precedence than exponentiation. - val expression34 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(4)") - assertThat(expression34).hasStructureThatMatches { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - assertThat(expression34).evaluatesToIntegerThat().isEqualTo(32) - - // An exponentiation can never be an implicit right operand. - expectFailureWhenParsingAlgebraicExpression("2^(3)2^2") - - val expression35 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)*2^2") - assertThat(expression35).hasStructureThatMatches { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression35).evaluatesToIntegerThat().isEqualTo(32) - - // An exponentiation can be a right operand of an implicit mult if it's grouped. - val expression36 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(2^2)") - assertThat(expression36).hasStructureThatMatches { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - rightOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - assertThat(expression36).evaluatesToIntegerThat().isEqualTo(32) - - // An exponentiation can never be an implicit right operand. - expectFailureWhenParsingAlgebraicExpression("2^3(4)2^3") - - val expression38 = parseAlgebraicExpressionWithoutOptionalErrors("2^3(4)*2^3") - assertThat(expression38).hasStructureThatMatches { - // 2^3(4)*2^3 - multiplication { - leftOperand { - // 2^3(4) - multiplication { - leftOperand { - // 2^3 - exponentiation { - leftOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - // 4 - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - rightOperand { - // 2^3 - exponentiation { - leftOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - assertThat(expression38).evaluatesToIntegerThat().isEqualTo(256) - - expectFailureWhenParsingAlgebraicExpression("2^2 2^2") - expectFailureWhenParsingAlgebraicExpression("(3) 2^2") - expectFailureWhenParsingAlgebraicExpression("sqrt(3) 2^2") - expectFailureWhenParsingAlgebraicExpression("√2 2^2") - expectFailureWhenParsingAlgebraicExpression("2^2 3") - - expectFailureWhenParsingAlgebraicExpression("-2 3") - - val expression39 = parseAlgebraicExpressionWithoutOptionalErrors("-(1+2)") - assertThat(expression39).hasStructureThatMatches { - negation { - operand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - assertThat(expression39).evaluatesToIntegerThat().isEqualTo(-3) - - // Should pass for algebra. - val expression66 = parseAlgebraicExpressionWithoutOptionalErrors("-2 x") - assertThat(expression66).hasStructureThatMatches { - negation { - operand { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - variable { - withNameThat().isEqualTo("x") - } - } - } - } - } - } - - val expression40 = parseAlgebraicExpressionWithoutOptionalErrors("-2 (1+2)") - assertThat(expression40).hasStructureThatMatches { - // The negation happens last for parity with other common calculators. - negation { - operand { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - } - assertThat(expression40).evaluatesToIntegerThat().isEqualTo(-6) - - val expression41 = parseAlgebraicExpressionWithoutOptionalErrors("-2^3(4)") - assertThat(expression41).hasStructureThatMatches { - negation { - operand { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - assertThat(expression41).evaluatesToIntegerThat().isEqualTo(-32) - - val expression43 = parseAlgebraicExpressionWithoutOptionalErrors("√2^2(3)") - assertThat(expression43).hasStructureThatMatches { - multiplication { - leftOperand { - exponentiation { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - assertThat(expression43).evaluatesToIrrationalThat().isWithin(1e-5).of(6.0) - - val expression60 = parseAlgebraicExpressionWithoutOptionalErrors("√(2^2(3))") - assertThat(expression60).hasStructureThatMatches { - functionCallTo(SQUARE_ROOT) { - argument { - group { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - } - } - assertThat(expression60).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(12.0)) - - val expression42 = parseAlgebraicExpressionWithoutOptionalErrors("-2*-2") - // Note that the following structure is not the same as (-2)*(-2) since unary negation has - // higher precedence than multiplication, so it's first & recurses to include the entire - // multiplication expression. - assertThat(expression42).hasStructureThatMatches { - negation { - operand { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - assertThat(expression42).evaluatesToIntegerThat().isEqualTo(4) - - val expression44 = parseAlgebraicExpressionWithoutOptionalErrors("2(2)") - assertThat(expression44).hasStructureThatMatches { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - assertThat(expression44).evaluatesToIntegerThat().isEqualTo(4) - - val expression45 = parseAlgebraicExpressionWithoutOptionalErrors("2sqrt(2)") - assertThat(expression45).hasStructureThatMatches { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression45).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) - - val expression46 = parseAlgebraicExpressionWithoutOptionalErrors("2√2") - assertThat(expression46).hasStructureThatMatches { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression46).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) - - val expression47 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)") - assertThat(expression47).hasStructureThatMatches { - multiplication { - leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - assertThat(expression47).evaluatesToIntegerThat().isEqualTo(4) - - val expression48 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)(2)") - assertThat(expression48).hasStructureThatMatches { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - assertThat(expression48).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) - - val expression49 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)sqrt(2)") - assertThat(expression49).hasStructureThatMatches { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression49).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) - - val expression50 = parseAlgebraicExpressionWithoutOptionalErrors("√2√2") - assertThat(expression50).hasStructureThatMatches { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression50).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) - - val expression51 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)(2)") - assertThat(expression51).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - assertThat(expression51).evaluatesToIntegerThat().isEqualTo(8) - - val expression52 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)sqrt(2)sqrt(2)") - assertThat(expression52).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - val sqrt2 = sqrt(2.0) - assertThat(expression52).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) - - val expression53 = parseAlgebraicExpressionWithoutOptionalErrors("√2√2√2") - assertThat(expression53).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression53).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) - - // Should fail for algebra. - expectFailureWhenParsingAlgebraicExpression("x7") - - // Should pass for algebra. - val expression67 = parseAlgebraicExpressionWithoutOptionalErrors("2x^2y^-3") - assertThat(expression67).hasStructureThatMatches { - // 2x^2y^-3 -> (2*(x^2))*(y^(-3)) - multiplication { - // 2x^2 - leftOperand { - multiplication { - // 2 - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - // x^2 - rightOperand { - exponentiation { - // x - leftOperand { - variable { - withNameThat().isEqualTo("x") - } - } - // 2 - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - // y^-3 - rightOperand { - exponentiation { - // y - leftOperand { - variable { - withNameThat().isEqualTo("y") - } - } - // -3 - rightOperand { - negation { - // 3 - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - } - } - - val expression54 = parseAlgebraicExpressionWithoutOptionalErrors("2*2/-4+7*2") - assertThat(expression54).hasStructureThatMatches { - // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) - addition { - leftOperand { - // 2*2/-4 - division { - leftOperand { - // 2*2 - multiplication { - leftOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - // -4 - negation { - // 4 - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - rightOperand { - // 7*2 - multiplication { - leftOperand { - // 7 - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - assertThat(expression54).evaluatesToIntegerThat().isEqualTo(13) - - val expression55 = parseAlgebraicExpressionWithoutOptionalErrors("3/(1-2)") - assertThat(expression55).hasStructureThatMatches { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - assertThat(expression55).evaluatesToIntegerThat().isEqualTo(-3) - - val expression56 = parseAlgebraicExpressionWithoutOptionalErrors("(3)/(1-2)") - assertThat(expression56).hasStructureThatMatches { - division { - leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - assertThat(expression56).evaluatesToIntegerThat().isEqualTo(-3) - - val expression57 = parseAlgebraicExpressionWithoutOptionalErrors("3/((1-2))") - assertThat(expression57).hasStructureThatMatches { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - group { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - assertThat(expression57).evaluatesToIntegerThat().isEqualTo(-3) - - // TODO: add others, including tests for malformed expressions throughout the parser & - // tokenizer. - } - - @Test - fun testLotsOfCasesForAlgebraicEquation() { - expectFailureWhenParsingAlgebraicEquation(" x =") - expectFailureWhenParsingAlgebraicEquation(" = y") - - val equation1 = parseAlgebraicEquationWithAllErrors("x = 1") - assertThat(equation1).hasLeftHandSideThat().hasStructureThatMatches { - variable { - withNameThat().isEqualTo("x") - } - } - assertThat(equation1).hasRightHandSideThat().evaluatesToIntegerThat().isEqualTo(1) - - val equation2 = - parseAlgebraicEquationWithAllErrors("y = mx + b", allowedVariables = listOf("x", "y", "b", "m")) - assertThat(equation2).hasLeftHandSideThat().hasStructureThatMatches { - variable { - withNameThat().isEqualTo("y") - } - } - assertThat(equation2).hasRightHandSideThat().hasStructureThatMatches { - addition { - leftOperand { - multiplication { - leftOperand { - variable { - withNameThat().isEqualTo("m") - } - } - rightOperand { - variable { - withNameThat().isEqualTo("x") - } - } - } - } - rightOperand { - variable { - withNameThat().isEqualTo("b") - } - } - } - } - - val equation3 = parseAlgebraicEquationWithAllErrors("y = (x+1)^2") - assertThat(equation3).hasLeftHandSideThat().hasStructureThatMatches { - variable { - withNameThat().isEqualTo("y") - } - } - assertThat(equation3).hasRightHandSideThat().hasStructureThatMatches { - exponentiation { - leftOperand { - group { - addition { - leftOperand { - variable { - withNameThat().isEqualTo("x") - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - - val equation4 = parseAlgebraicEquationWithAllErrors("y = (x+1)(x-1)") - assertThat(equation4).hasLeftHandSideThat().hasStructureThatMatches { - variable { - withNameThat().isEqualTo("y") - } - } - assertThat(equation4).hasRightHandSideThat().hasStructureThatMatches { - multiplication { - leftOperand { - group { - addition { - leftOperand { - variable { - withNameThat().isEqualTo("x") - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - } - } - } - rightOperand { - group { - subtraction { - leftOperand { - variable { - withNameThat().isEqualTo("x") - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - } - } - } - } - } - - expectFailureWhenParsingAlgebraicEquation("y = (x+1)(x-1) 2") - expectFailureWhenParsingAlgebraicEquation("y 2 = (x+1)(x-1)") - - val equation5 = - parseAlgebraicEquationWithAllErrors("a*x^2 + b*x + c = 0", allowedVariables = listOf("x", "a", "b", "c")) - assertThat(equation5).hasLeftHandSideThat().hasStructureThatMatches { - addition { - leftOperand { - addition { - leftOperand { - multiplication { - leftOperand { - variable { - withNameThat().isEqualTo("a") - } - } - rightOperand { - exponentiation { - leftOperand { - variable { - withNameThat().isEqualTo("x") - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - rightOperand { - multiplication { - leftOperand { - variable { - withNameThat().isEqualTo("b") - } - } - rightOperand { - variable { - withNameThat().isEqualTo("x") - } - } - } - } - } - } - rightOperand { - variable { - withNameThat().isEqualTo("c") - } - } - } - } - assertThat(equation5).hasRightHandSideThat().hasStructureThatMatches { - constant { - withValueThat().isIntegerThat().isEqualTo(0) - } - } - } - - @Test - fun testLatex() { - // TODO: split up & move to separate test suites. Finish test cases. - - val exp1 = parseNumericExpressionWithAllErrors("1") - assertThat(exp1).convertsToLatexStringThat().isEqualTo("1") - - val exp2 = parseNumericExpressionWithAllErrors("1+2") - assertThat(exp2).convertsToLatexStringThat().isEqualTo("1 + 2") - - val exp3 = parseNumericExpressionWithAllErrors("1*2") - assertThat(exp3).convertsToLatexStringThat().isEqualTo("1 \\times 2") - - val exp4 = parseNumericExpressionWithAllErrors("1/2") - assertThat(exp4).convertsToLatexStringThat().isEqualTo("1 \\div 2") - - val exp5 = parseNumericExpressionWithAllErrors("1/2") - assertThat(exp5).convertsWithFractionsToLatexStringThat().isEqualTo("\\frac{1}{2}") - - val exp10 = parseNumericExpressionWithAllErrors("√2") - assertThat(exp10).convertsToLatexStringThat().isEqualTo("\\sqrt{2}") - - val exp11 = parseNumericExpressionWithAllErrors("√(1/2)") - assertThat(exp11).convertsToLatexStringThat().isEqualTo("\\sqrt{(1 \\div 2)}") - - val exp6 = parseAlgebraicExpressionWithAllErrors("x+y") - assertThat(exp6).convertsToLatexStringThat().isEqualTo("x + y") - - val exp7 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1/y)") - assertThat(exp7).convertsToLatexStringThat().isEqualTo("x ^ {(1 \\div y)}") - - val exp8 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1/y)") - assertThat(exp8).convertsWithFractionsToLatexStringThat().isEqualTo("x ^ {(\\frac{1}{y})}") - - val exp9 = parseAlgebraicExpressionWithoutOptionalErrors("x^y^z") - assertThat(exp9).convertsWithFractionsToLatexStringThat().isEqualTo("x ^ {y ^ {z}}") - - val eq1 = - parseAlgebraicEquationWithAllErrors( - "7a^2+b^2+c^2=0", allowedVariables = listOf("a", "b", "c") - ) - assertThat(eq1).convertsToLatexStringThat().isEqualTo("7a ^ {2} + b ^ {2} + c ^ {2} = 0") - - val eq2 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") - assertThat(eq2).convertsToLatexStringThat().isEqualTo("\\sqrt{1 + x} \\div x = 1") - - val eq3 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") - assertThat(eq3) - .convertsWithFractionsToLatexStringThat() - .isEqualTo("\\frac{\\sqrt{1 + x}}{x} = 1") - } - - @Test - fun testHumanReadableString() { - // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). - - val exp1 = parseNumericExpressionWithAllErrors("1") - assertThat(exp1).forHumanReadable(ARABIC).doesNotConvertToString() - - assertThat(exp1).forHumanReadable(HINDI).doesNotConvertToString() - - assertThat(exp1).forHumanReadable(HINGLISH).doesNotConvertToString() - - assertThat(exp1).forHumanReadable(PORTUGUESE).doesNotConvertToString() - - assertThat(exp1).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() - - assertThat(exp1).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() - - assertThat(exp1).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() - - val exp2 = parseAlgebraicExpressionWithAllErrors("x") - assertThat(exp2).forHumanReadable(ARABIC).doesNotConvertToString() - - assertThat(exp2).forHumanReadable(HINDI).doesNotConvertToString() - - assertThat(exp2).forHumanReadable(HINGLISH).doesNotConvertToString() - - assertThat(exp2).forHumanReadable(PORTUGUESE).doesNotConvertToString() - - assertThat(exp2).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() - - assertThat(exp2).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() - - assertThat(exp2).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() - - val eq1 = parseAlgebraicEquationWithAllErrors("x=1") - assertThat(eq1).forHumanReadable(ARABIC).doesNotConvertToString() - - assertThat(eq1).forHumanReadable(HINDI).doesNotConvertToString() - - assertThat(eq1).forHumanReadable(HINGLISH).doesNotConvertToString() - - assertThat(eq1).forHumanReadable(PORTUGUESE).doesNotConvertToString() - - assertThat(eq1).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() - - assertThat(eq1).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() - - assertThat(eq1).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() - - // specific cases (from rules & other cases): - val exp3 = parseNumericExpressionWithAllErrors("1") - assertThat(exp3).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") - assertThat(exp3).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") - - val exp49 = parseNumericExpressionWithAllErrors("-1") - assertThat(exp49).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative 1") - - val exp50 = parseNumericExpressionWithAllErrors("+1") - assertThat(exp50).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive 1") - - val exp4 = parseNumericExpressionWithoutOptionalErrors("((1))") - assertThat(exp4).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") - - val exp5 = parseNumericExpressionWithAllErrors("1+2") - assertThat(exp5).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus 2") - - val exp6 = parseNumericExpressionWithAllErrors("1-2") - assertThat(exp6).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus 2") - - val exp7 = parseNumericExpressionWithAllErrors("1*2") - assertThat(exp7).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times 2") - - val exp8 = parseNumericExpressionWithAllErrors("1/2") - assertThat(exp8).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by 2") - - val exp9 = parseNumericExpressionWithAllErrors("1+(1-2)") - assertThat(exp9) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("1 plus open parenthesis 1 minus 2 close parenthesis") - - val exp10 = parseNumericExpressionWithAllErrors("2^3") - assertThat(exp10) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("2 raised to the power of 3") - - val exp11 = parseNumericExpressionWithAllErrors("2^(1+2)") - assertThat(exp11) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("2 raised to the power of open parenthesis 1 plus 2 close parenthesis") - - val exp12 = parseNumericExpressionWithAllErrors("100000*2") - assertThat(exp12).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") - - val exp13 = parseNumericExpressionWithAllErrors("sqrt(2)") - assertThat(exp13).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") - - val exp14 = parseNumericExpressionWithAllErrors("√2") - assertThat(exp14).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") - - val exp15 = parseNumericExpressionWithAllErrors("sqrt(1+2)") - assertThat(exp15) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("start square root 1 plus 2 end square root") - - val singularOrdinalNames = mapOf( - 1 to "oneth", - 2 to "half", - 3 to "third", - 4 to "fourth", - 5 to "fifth", - 6 to "sixth", - 7 to "seventh", - 8 to "eighth", - 9 to "ninth", - 10 to "tenth", - ) - val pluralOrdinalNames = mapOf( - 1 to "oneths", - 2 to "halves", - 3 to "thirds", - 4 to "fourths", - 5 to "fifths", - 6 to "sixths", - 7 to "sevenths", - 8 to "eighths", - 9 to "ninths", - 10 to "tenths", - ) - for (denominatorToCheck in 1..10) { - for (numeratorToCheck in 0..denominatorToCheck) { - val exp16 = parseNumericExpressionWithAllErrors("$numeratorToCheck/$denominatorToCheck") - - val ordinalName = - if (numeratorToCheck == 1) { - singularOrdinalNames.getValue(denominatorToCheck) - } else pluralOrdinalNames.getValue(denominatorToCheck) - assertThat(exp16) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("$numeratorToCheck $ordinalName") - } - } - - val exp17 = parseNumericExpressionWithAllErrors("-1/3") - assertThat(exp17) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("negative 1 third") - - val exp18 = parseNumericExpressionWithAllErrors("-2/3") - assertThat(exp18) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("negative 2 thirds") - - val exp19 = parseNumericExpressionWithAllErrors("10/11") - assertThat(exp19) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("10 over 11") - - val exp20 = parseNumericExpressionWithAllErrors("121/7986") - assertThat(exp20) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("121 over 7,986") - - val exp21 = parseNumericExpressionWithAllErrors("8/7") - assertThat(exp21) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("8 over 7") - - val exp22 = parseNumericExpressionWithAllErrors("-10/-30") - assertThat(exp22) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("negative the fraction with numerator 10 and denominator negative 30") - - val exp23 = parseAlgebraicExpressionWithAllErrors("1") - assertThat(exp23).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") - - val exp24 = parseAlgebraicExpressionWithoutOptionalErrors("((1))") - assertThat(exp24).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") - - val exp25 = parseAlgebraicExpressionWithAllErrors("x") - assertThat(exp25).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") - - val exp26 = parseAlgebraicExpressionWithoutOptionalErrors("((x))") - assertThat(exp26).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") - - val exp51 = parseAlgebraicExpressionWithAllErrors("-x") - assertThat(exp51).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative x") - - val exp52 = parseAlgebraicExpressionWithAllErrors("+x") - assertThat(exp52).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive x") - - val exp27 = parseAlgebraicExpressionWithAllErrors("1+x") - assertThat(exp27).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus x") - - val exp28 = parseAlgebraicExpressionWithAllErrors("1-x") - assertThat(exp28).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus x") - - val exp29 = parseAlgebraicExpressionWithAllErrors("1*x") - assertThat(exp29).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times x") - - val exp30 = parseAlgebraicExpressionWithAllErrors("1/x") - assertThat(exp30).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by x") - - val exp31 = parseAlgebraicExpressionWithAllErrors("1/x") - assertThat(exp31) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("the fraction with numerator 1 and denominator x") - - val exp32 = parseAlgebraicExpressionWithAllErrors("1+(1-x)") - assertThat(exp32) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("1 plus open parenthesis 1 minus x close parenthesis") - - val exp33 = parseAlgebraicExpressionWithAllErrors("2x") - assertThat(exp33).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x") - - val exp34 = parseAlgebraicExpressionWithAllErrors("xy") - assertThat(exp34).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x times y") - - val exp35 = parseAlgebraicExpressionWithAllErrors("z") - assertThat(exp35).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("zed") - - val exp36 = parseAlgebraicExpressionWithAllErrors("2xz") - assertThat(exp36).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x times zed") - - val exp37 = parseAlgebraicExpressionWithAllErrors("x^2") - assertThat(exp37) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("x raised to the power of 2") - - val exp38 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1+x)") - assertThat(exp38) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("x raised to the power of open parenthesis 1 plus x close parenthesis") - - val exp39 = parseAlgebraicExpressionWithAllErrors("100000*2") - assertThat(exp39).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") - - val exp40 = parseAlgebraicExpressionWithAllErrors("sqrt(2)") - assertThat(exp40).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") - - val exp41 = parseAlgebraicExpressionWithAllErrors("sqrt(x)") - assertThat(exp41).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") - - val exp42 = parseAlgebraicExpressionWithAllErrors("√2") - assertThat(exp42).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") - - val exp43 = parseAlgebraicExpressionWithAllErrors("√x") - assertThat(exp43).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") - - val exp44 = parseAlgebraicExpressionWithAllErrors("sqrt(1+2)") - assertThat(exp44) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("start square root 1 plus 2 end square root") - - val exp45 = parseAlgebraicExpressionWithAllErrors("sqrt(1+x)") - assertThat(exp45) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("start square root 1 plus x end square root") - - val exp46 = parseAlgebraicExpressionWithAllErrors("√(1+x)") - assertThat(exp46) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("start square root open parenthesis 1 plus x close parenthesis end square root") - - for (denominatorToCheck in 1..10) { - for (numeratorToCheck in 0..denominatorToCheck) { - val exp16 = parseAlgebraicExpressionWithAllErrors("$numeratorToCheck/$denominatorToCheck") - - val ordinalName = - if (numeratorToCheck == 1) { - singularOrdinalNames.getValue(denominatorToCheck) - } else pluralOrdinalNames.getValue(denominatorToCheck) - assertThat(exp16) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("$numeratorToCheck $ordinalName") - } - } - - val exp47 = parseAlgebraicExpressionWithAllErrors("1") - assertThat(exp47).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") - - val exp48 = parseAlgebraicExpressionWithAllErrors("x(5-y)") - assertThat(exp48) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("x times open parenthesis 5 minus y close parenthesis") - - val eq2 = parseAlgebraicEquationWithAllErrors("x=1/y") - assertThat(eq2) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("x equals 1 divided by y") - - val eq3 = parseAlgebraicEquationWithAllErrors("x=1/2") - assertThat(eq3) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("x equals 1 divided by 2") - - val eq4 = parseAlgebraicEquationWithAllErrors("x=1/y") - assertThat(eq4) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("x equals the fraction with numerator 1 and denominator y") - - val eq5 = parseAlgebraicEquationWithAllErrors("x=1/2") - assertThat(eq5) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("x equals 1 half") - - // Tests from examples in the PRD - val eq6 = parseAlgebraicEquationWithAllErrors("3x^2+4y=62") - assertThat(eq6) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("3 x raised to the power of 2 plus 4 y equals 62") - - val exp53 = parseAlgebraicExpressionWithAllErrors("(x+6)/(x-4)") - assertThat(exp53) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo( - "the fraction with numerator open parenthesis x plus 6 close parenthesis and denominator" + - " open parenthesis x minus 4 close parenthesis" - ) - - val exp54 = parseAlgebraicExpressionWithoutOptionalErrors("4*(x)^(2)+20x") - assertThat(exp54) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("4 times x raised to the power of 2 plus 20 x") - - val exp55 = parseAlgebraicExpressionWithAllErrors("3+x-5") - assertThat(exp55).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("3 plus x minus 5") - - val exp56 = parseAlgebraicExpressionWithAllErrors("Z+A-Z", allowedVariables = listOf("A", "Z")) - assertThat(exp56).forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("Zed plus A minus Zed") - - val exp57 = - parseAlgebraicExpressionWithAllErrors("6C-5A-1", allowedVariables = listOf("A", "C")) - assertThat(exp57) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("6 C minus 5 A minus 1") - - val exp58 = parseAlgebraicExpressionWithAllErrors("5*Z-w", allowedVariables = listOf("Z", "w")) - assertThat(exp58) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("5 times Zed minus w") - - val exp59 = - parseAlgebraicExpressionWithAllErrors("L*S-3S+L", allowedVariables = listOf("L", "S")) - assertThat(exp59) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("L times S minus 3 S plus L") - - val exp60 = parseAlgebraicExpressionWithAllErrors("2*(2+6+3+4)") - assertThat(exp60) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("2 times open parenthesis 2 plus 6 plus 3 plus 4 close parenthesis") - - val exp61 = parseAlgebraicExpressionWithAllErrors("sqrt(64)") - assertThat(exp61) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("square root of 64") - - val exp62 = parseAlgebraicExpressionWithAllErrors("√(a+b)", allowedVariables = listOf("a", "b")) - assertThat(exp62) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("start square root open parenthesis a plus b close parenthesis end square root") - - val exp63 = parseAlgebraicExpressionWithAllErrors("3*10^-5") - assertThat(exp63) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("3 times 10 raised to the power of negative 5") - - val exp64 = - parseAlgebraicExpressionWithoutOptionalErrors( - "((x+2y)+5*(a-2b)+z)", allowedVariables = listOf("x", "y", "a", "b", "z") - ) - assertThat(exp64) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo( - "open parenthesis open parenthesis x plus 2 y close parenthesis plus 5 times open" + - " parenthesis a minus 2 b close parenthesis plus zed close parenthesis" - ) - } - - @Test - fun testToComparableOperation() { - // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). - - val exp1 = parseNumericExpressionWithAllErrors("1") - assertThat(exp1.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - - val exp2 = parseNumericExpressionWithAllErrors("-1") - assertThat(exp2.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - - val exp3 = parseNumericExpressionWithAllErrors("1+3+4") - assertThat(exp3.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - - val exp4 = parseNumericExpressionWithAllErrors("-1-2-3") - assertThat(exp4.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(2) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - - val exp5 = parseNumericExpressionWithAllErrors("1+2-3") - assertThat(exp5.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(2) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - - val exp6 = parseNumericExpressionWithAllErrors("2*3*4") - assertThat(exp6.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - - val exp7 = parseNumericExpressionWithAllErrors("1-2*3") - assertThat(exp7.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - } - } - - val exp8 = parseNumericExpressionWithAllErrors("2*3-4") - assertThat(exp8.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - - val exp9 = parseNumericExpressionWithAllErrors("1+2*3-4+8*7*6-9") - assertThat(exp9.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(6) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(8) - } - } - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(3) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(4) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(9) - } - } - } - } - - val exp10 = parseNumericExpressionWithAllErrors("2/3/4") - assertThat(exp10.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - - val exp11 = parseNumericExpressionWithoutOptionalErrors("2^3^4") - assertThat(exp11.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - } - - val exp12 = parseNumericExpressionWithAllErrors("1+2/3+3") - assertThat(exp12.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - - val exp13 = parseNumericExpressionWithAllErrors("1+(2/3)+3") - assertThat(exp13.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - - val exp14 = parseNumericExpressionWithAllErrors("1+2^3+3") - assertThat(exp14.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - - val exp15 = parseNumericExpressionWithAllErrors("1+(2^3)+3") - assertThat(exp15.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - - val exp16 = parseNumericExpressionWithAllErrors("2*3/4*7") - assertThat(exp16.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(4) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - - val exp17 = parseNumericExpressionWithAllErrors("2*(3/4)*7") - assertThat(exp17.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(4) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - - val exp18 = parseNumericExpressionWithAllErrors("-3*sqrt(2)") - assertThat(exp18.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - squareRootWithArgument { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - - val exp19 = parseNumericExpressionWithAllErrors("1+(2+(3+(4+5)))") - assertThat(exp19.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(4) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - - val exp20 = parseNumericExpressionWithAllErrors("2*(3*(4*(5*6)))") - assertThat(exp20.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - index(4) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(6) - } - } - } - } - - val exp21 = parseAlgebraicExpressionWithAllErrors("x") - assertThat(exp21.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - - val exp22 = parseAlgebraicExpressionWithAllErrors("-x") - assertThat(exp22.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - - val exp23 = parseAlgebraicExpressionWithAllErrors("1+x+y") - assertThat(exp23.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - - val exp24 = parseAlgebraicExpressionWithAllErrors("-1-x-y") - assertThat(exp24.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - - val exp25 = parseAlgebraicExpressionWithAllErrors("1+x-y") - assertThat(exp25.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - - val exp26 = parseAlgebraicExpressionWithAllErrors("2xy") - assertThat(exp26.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - - val exp27 = parseAlgebraicExpressionWithAllErrors("1-xy") - assertThat(exp27.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - } - } - - val exp28 = parseAlgebraicExpressionWithAllErrors("xy-4") - assertThat(exp28.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - - val exp29 = parseAlgebraicExpressionWithAllErrors("1+xy-4+yz-9") - assertThat(exp29.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(3) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(4) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(9) - } - } - } - } - - val exp30 = parseAlgebraicExpressionWithAllErrors("2/x/y") - assertThat(exp30.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - - val exp31 = parseAlgebraicExpressionWithoutOptionalErrors("x^3^4") - assertThat(exp31.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - } - - val exp32 = parseAlgebraicExpressionWithAllErrors("1+x/y+z") - assertThat(exp32.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - - val exp33 = parseAlgebraicExpressionWithAllErrors("1+(x/y)+z") - assertThat(exp33.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - - val exp34 = parseAlgebraicExpressionWithAllErrors("1+x^3+y") - assertThat(exp34.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - - val exp35 = parseAlgebraicExpressionWithAllErrors("1+(x^3)+y") - assertThat(exp35.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - - val exp36 = parseAlgebraicExpressionWithAllErrors("2*x/y*z") - assertThat(exp36.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(4) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - - val exp37 = parseAlgebraicExpressionWithAllErrors("2*(x/y)*z") - assertThat(exp37.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(4) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - - val exp38 = parseAlgebraicExpressionWithAllErrors("-2*sqrt(x)") - assertThat(exp38.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - squareRootWithArgument { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - - val exp39 = parseAlgebraicExpressionWithAllErrors("1+(x+(3+(z+y)))") - assertThat(exp39.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - index(4) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - - val exp40 = parseAlgebraicExpressionWithAllErrors("2*(x*(4*(zy)))") - assertThat(exp40.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - index(4) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - - // Equality tests: - val list1 = createComparableOperationListFromNumericExpression("(1+2)+3") - val list2 = createComparableOperationListFromNumericExpression("1+(2+3)") - assertThat(list1).isEqualTo(list2) - - val list3 = createComparableOperationListFromNumericExpression("1+2+3") - val list4 = createComparableOperationListFromNumericExpression("3+2+1") - assertThat(list3).isEqualTo(list4) - - val list5 = createComparableOperationListFromNumericExpression("1-2-3") - val list6 = createComparableOperationListFromNumericExpression("-3 + -2 + 1") - assertThat(list5).isEqualTo(list6) - - val list7 = createComparableOperationListFromNumericExpression("1-2-3") - val list8 = createComparableOperationListFromNumericExpression("-3-2+1") - assertThat(list7).isEqualTo(list8) - - val list9 = createComparableOperationListFromNumericExpression("1-2-3") - val list10 = createComparableOperationListFromNumericExpression("-3-2+1") - assertThat(list9).isEqualTo(list10) - - val list11 = createComparableOperationListFromNumericExpression("1-2-3") - val list12 = createComparableOperationListFromNumericExpression("3-2-1") - assertThat(list11).isNotEqualTo(list12) - - val list13 = createComparableOperationListFromNumericExpression("2*3*4") - val list14 = createComparableOperationListFromNumericExpression("4*3*2") - assertThat(list13).isEqualTo(list14) - - val list15 = createComparableOperationListFromNumericExpression("2*(3/4)") - val list16 = createComparableOperationListFromNumericExpression("3/4*2") - assertThat(list15).isEqualTo(list16) - - val list17 = createComparableOperationListFromNumericExpression("2*3/4") - val list18 = createComparableOperationListFromNumericExpression("3/4*2") - assertThat(list17).isEqualTo(list18) - - val list45 = createComparableOperationListFromNumericExpression("2*3/4") - val list46 = createComparableOperationListFromNumericExpression("2*3*4") - assertThat(list45).isNotEqualTo(list46) - - val list19 = createComparableOperationListFromNumericExpression("2*3/4") - val list20 = createComparableOperationListFromNumericExpression("2*4/3") - assertThat(list19).isNotEqualTo(list20) - - val list21 = createComparableOperationListFromNumericExpression("2*3/4*7") - val list22 = createComparableOperationListFromNumericExpression("3/4*7*2") - assertThat(list21).isEqualTo(list22) - - val list23 = createComparableOperationListFromNumericExpression("2*3/4*7") - val list24 = createComparableOperationListFromNumericExpression("7*(3*2/4)") - assertThat(list23).isEqualTo(list24) - - val list25 = createComparableOperationListFromNumericExpression("2*3/4*7") - val list26 = createComparableOperationListFromNumericExpression("7*3*2/4") - assertThat(list25).isEqualTo(list26) - - val list27 = createComparableOperationListFromNumericExpression("-2*3") - val list28 = createComparableOperationListFromNumericExpression("3*-2") - assertThat(list27).isEqualTo(list28) - - val list29 = createComparableOperationListFromNumericExpression("2^3") - val list30 = createComparableOperationListFromNumericExpression("3^2") - assertThat(list29).isNotEqualTo(list30) - - val list31 = createComparableOperationListFromNumericExpression("-(1+2)") - val list32 = createComparableOperationListFromNumericExpression("-1+2") - assertThat(list31).isNotEqualTo(list32) - - val list33 = createComparableOperationListFromNumericExpression("-(1+2)") - val list34 = createComparableOperationListFromNumericExpression("-1-2") - assertThat(list33).isNotEqualTo(list34) - - val list35 = createComparableOperationListFromAlgebraicExpression("x(x+1)") - val list36 = createComparableOperationListFromAlgebraicExpression("(1+x)x") - assertThat(list35).isEqualTo(list36) - - val list37 = createComparableOperationListFromAlgebraicExpression("x(x+1)") - val list38 = createComparableOperationListFromAlgebraicExpression("x^2+x") - assertThat(list37).isNotEqualTo(list38) - - val list39 = createComparableOperationListFromAlgebraicExpression("x^2*sqrt(x)") - val list40 = createComparableOperationListFromAlgebraicExpression("x") - assertThat(list39).isNotEqualTo(list40) - - val list41 = createComparableOperationListFromAlgebraicExpression("xyz") - val list42 = createComparableOperationListFromAlgebraicExpression("zyx") - assertThat(list41).isEqualTo(list42) - - val list43 = createComparableOperationListFromAlgebraicExpression("1+xy-2") - val list44 = createComparableOperationListFromAlgebraicExpression("-2+1+yx") - assertThat(list43).isEqualTo(list44) - - // TODO: add tests for comparator/sorting & negation simplification? - } - - @Test - fun testPolynomials() { - // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). - - val poly1 = parseNumericExpressionWithAllErrors("1").toPolynomial() - assertThat(poly1).evaluatesToPlainTextThat().isEqualTo("1") - assertThat(poly1).isConstantThat().isIntegerThat().isEqualTo(1) - - val poly13 = parseNumericExpressionWithAllErrors("1-1").toPolynomial() - assertThat(poly13).evaluatesToPlainTextThat().isEqualTo("0") - assertThat(poly13).isConstantThat().isIntegerThat().isEqualTo(0) - - val poly2 = parseNumericExpressionWithAllErrors("3 + 4 * 2 / (1 - 5) ^ 2").toPolynomial() - assertThat(poly2).evaluatesToPlainTextThat().isEqualTo("7/2") - assertThat(poly2).isConstantThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(3) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(2) - } - - val poly3 = parseAlgebraicExpressionWithAllErrors("133+3.14*x/(11-15)^2").toPolynomial() - assertThat(poly3).evaluatesToPlainTextThat().isEqualTo("0.19625x + 133") - assertThat(poly3).hasTermCountThat().isEqualTo(2) - assertThat(poly3).term(0).hasCoefficientThat().isIrrationalThat().isWithin(1e-5).of(0.19625) - assertThat(poly3).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly3).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly3).term(0).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly3).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(133) - assertThat(poly3).term(1).hasVariableCountThat().isEqualTo(0) - - val poly4 = parseAlgebraicExpressionWithAllErrors("x^2").toPolynomial() - assertThat(poly4).evaluatesToPlainTextThat().isEqualTo("x^2") - assertThat(poly4).hasTermCountThat().isEqualTo(1) - assertThat(poly4).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly4).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly4).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly4).term(0).variable(0).hasPowerThat().isEqualTo(2) - - val poly5 = parseAlgebraicExpressionWithAllErrors("xy+x").toPolynomial() - assertThat(poly5).evaluatesToPlainTextThat().isEqualTo("xy + x") - assertThat(poly5).hasTermCountThat().isEqualTo(2) - assertThat(poly5).term(0).hasVariableCountThat().isEqualTo(2) - assertThat(poly5).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly5).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly5).term(0).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly5).term(0).variable(1).hasNameThat().isEqualTo("y") - assertThat(poly5).term(0).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly5).term(1).hasVariableCountThat().isEqualTo(1) - assertThat(poly5).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly5).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly5).term(1).variable(0).hasPowerThat().isEqualTo(1) - - val poly6 = parseAlgebraicExpressionWithAllErrors("2x").toPolynomial() - assertThat(poly6).evaluatesToPlainTextThat().isEqualTo("2x") - assertThat(poly6).hasTermCountThat().isEqualTo(1) - assertThat(poly6).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly6).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) - assertThat(poly6).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly6).term(0).variable(0).hasPowerThat().isEqualTo(1) - - val poly30 = parseAlgebraicExpressionWithAllErrors("x+2").toPolynomial() - assertThat(poly30).evaluatesToPlainTextThat().isEqualTo("x + 2") - assertThat(poly30).hasTermCountThat().isEqualTo(2) - assertThat(poly30).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly30).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(2) - hasVariableCountThat().isEqualTo(0) - } - - val poly29 = parseAlgebraicExpressionWithAllErrors("x^2-3*x-10").toPolynomial() - assertThat(poly29).evaluatesToPlainTextThat().isEqualTo("x^2 - 3x - 10") - assertThat(poly29).hasTermCountThat().isEqualTo(3) - assertThat(poly29).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly29).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-3) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly29).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-10) - hasVariableCountThat().isEqualTo(0) - } - - val poly31 = parseAlgebraicExpressionWithAllErrors("4*(x+2)").toPolynomial() - assertThat(poly31).evaluatesToPlainTextThat().isEqualTo("4x + 8") - assertThat(poly31).hasTermCountThat().isEqualTo(2) - assertThat(poly31).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(4) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly31).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(8) - hasVariableCountThat().isEqualTo(0) - } - - val poly7 = parseAlgebraicExpressionWithAllErrors("2xy^2z^3").toPolynomial() - assertThat(poly7).evaluatesToPlainTextThat().isEqualTo("2xy^2z^3") - assertThat(poly7).hasTermCountThat().isEqualTo(1) - assertThat(poly7).term(0).hasVariableCountThat().isEqualTo(3) - assertThat(poly7).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) - assertThat(poly7).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly7).term(0).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly7).term(0).variable(1).hasNameThat().isEqualTo("y") - assertThat(poly7).term(0).variable(1).hasPowerThat().isEqualTo(2) - assertThat(poly7).term(0).variable(2).hasNameThat().isEqualTo("z") - assertThat(poly7).term(0).variable(2).hasPowerThat().isEqualTo(3) - - // Show that 7+xy+yz-3-xz-yz+3xy-4 combines into 4xy-xz (the eliminated terms should be gone). - val poly8 = parseAlgebraicExpressionWithAllErrors("xy+yz-xz-yz+3xy").toPolynomial() - assertThat(poly8).evaluatesToPlainTextThat().isEqualTo("4xy - xz") - assertThat(poly8).hasTermCountThat().isEqualTo(2) - assertThat(poly8).term(0).hasVariableCountThat().isEqualTo(2) - assertThat(poly8).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(4) - assertThat(poly8).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly8).term(0).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly8).term(0).variable(1).hasNameThat().isEqualTo("y") - assertThat(poly8).term(0).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly8).term(1).hasVariableCountThat().isEqualTo(2) - assertThat(poly8).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(-1) - assertThat(poly8).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly8).term(1).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly8).term(1).variable(1).hasNameThat().isEqualTo("z") - assertThat(poly8).term(1).variable(1).hasPowerThat().isEqualTo(1) - - // x+2x should become 3x since like terms are combined. - val poly9 = parseAlgebraicExpressionWithAllErrors("x+2x").toPolynomial() - assertThat(poly9).evaluatesToPlainTextThat().isEqualTo("3x") - assertThat(poly9).hasTermCountThat().isEqualTo(1) - assertThat(poly9).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly9).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(3) - assertThat(poly9).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly9).term(0).variable(0).hasPowerThat().isEqualTo(1) - - // xx^2 should become x^3 since like terms are combined. - val poly10 = parseAlgebraicExpressionWithAllErrors("xx^2").toPolynomial() - assertThat(poly10).evaluatesToPlainTextThat().isEqualTo("x^3") - assertThat(poly10).hasTermCountThat().isEqualTo(1) - assertThat(poly10).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly10).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly10).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly10).term(0).variable(0).hasPowerThat().isEqualTo(3) - - // No terms in this polynomial should be combined. - val poly11 = parseAlgebraicExpressionWithAllErrors("x^2+x+1").toPolynomial() - assertThat(poly11).evaluatesToPlainTextThat().isEqualTo("x^2 + x + 1") - assertThat(poly11).hasTermCountThat().isEqualTo(3) - assertThat(poly11).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly11).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly11).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly11).term(0).variable(0).hasPowerThat().isEqualTo(2) - assertThat(poly11).term(1).hasVariableCountThat().isEqualTo(1) - assertThat(poly11).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly11).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly11).term(1).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly11).term(2).hasVariableCountThat().isEqualTo(0) - assertThat(poly11).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) - - // No terms in this polynomial should be combined. - val poly12 = parseAlgebraicExpressionWithAllErrors("x^2 + x^2y").toPolynomial() - assertThat(poly12).evaluatesToPlainTextThat().isEqualTo("x^2y + x^2") - assertThat(poly12).hasTermCountThat().isEqualTo(2) - assertThat(poly12).term(0).hasVariableCountThat().isEqualTo(2) - assertThat(poly12).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly12).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly12).term(0).variable(0).hasPowerThat().isEqualTo(2) - assertThat(poly12).term(0).variable(1).hasNameThat().isEqualTo("y") - assertThat(poly12).term(0).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly12).term(1).hasVariableCountThat().isEqualTo(1) - assertThat(poly12).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly12).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly12).term(1).variable(0).hasPowerThat().isEqualTo(2) - - // Ordering tests. Verify that ordering matches - // https://en.wikipedia.org/wiki/Polynomial#Definition (where multiple variables are sorted - // lexicographically). - - // The order of the terms in this polynomial should be reversed. - val poly14 = parseAlgebraicExpressionWithAllErrors("1+x+x^2+x^3").toPolynomial() - assertThat(poly14).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") - assertThat(poly14).hasTermCountThat().isEqualTo(4) - assertThat(poly14).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly14).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly14).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly14).term(0).variable(0).hasPowerThat().isEqualTo(3) - assertThat(poly14).term(1).hasVariableCountThat().isEqualTo(1) - assertThat(poly14).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly14).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly14).term(1).variable(0).hasPowerThat().isEqualTo(2) - assertThat(poly14).term(2).hasVariableCountThat().isEqualTo(1) - assertThat(poly14).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly14).term(2).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly14).term(2).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly14).term(3).hasVariableCountThat().isEqualTo(0) - assertThat(poly14).term(3).hasCoefficientThat().isIntegerThat().isEqualTo(1) - - // The order of the terms in this polynomial should be preserved. - val poly15 = parseAlgebraicExpressionWithAllErrors("x^3+x^2+x+1").toPolynomial() - assertThat(poly15).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") - assertThat(poly15).hasTermCountThat().isEqualTo(4) - assertThat(poly15).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly15).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly15).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly15).term(0).variable(0).hasPowerThat().isEqualTo(3) - assertThat(poly15).term(1).hasVariableCountThat().isEqualTo(1) - assertThat(poly15).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly15).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly15).term(1).variable(0).hasPowerThat().isEqualTo(2) - assertThat(poly15).term(2).hasVariableCountThat().isEqualTo(1) - assertThat(poly15).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly15).term(2).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly15).term(2).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly15).term(3).hasVariableCountThat().isEqualTo(0) - assertThat(poly15).term(3).hasCoefficientThat().isIntegerThat().isEqualTo(1) - - // The order of the terms in this polynomial should be reversed. - val poly16 = parseAlgebraicExpressionWithAllErrors("xy+xz+yz").toPolynomial() - assertThat(poly16).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") - assertThat(poly16).hasTermCountThat().isEqualTo(3) - assertThat(poly16).term(0).hasVariableCountThat().isEqualTo(2) - assertThat(poly16).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly16).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly16).term(0).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly16).term(0).variable(1).hasNameThat().isEqualTo("y") - assertThat(poly16).term(0).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly16).term(1).hasVariableCountThat().isEqualTo(2) - assertThat(poly16).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly16).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly16).term(1).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly16).term(1).variable(1).hasNameThat().isEqualTo("z") - assertThat(poly16).term(1).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly16).term(2).hasVariableCountThat().isEqualTo(2) - assertThat(poly16).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly16).term(2).variable(0).hasNameThat().isEqualTo("y") - assertThat(poly16).term(2).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly16).term(2).variable(1).hasNameThat().isEqualTo("z") - assertThat(poly16).term(2).variable(1).hasPowerThat().isEqualTo(1) - - // The order of the terms in this polynomial should be preserved. - val poly17 = parseAlgebraicExpressionWithAllErrors("yz+xz+xy").toPolynomial() - assertThat(poly17).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") - assertThat(poly17).hasTermCountThat().isEqualTo(3) - assertThat(poly17).term(0).hasVariableCountThat().isEqualTo(2) - assertThat(poly17).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly17).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly17).term(0).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly17).term(0).variable(1).hasNameThat().isEqualTo("y") - assertThat(poly17).term(0).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly17).term(1).hasVariableCountThat().isEqualTo(2) - assertThat(poly17).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly17).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly17).term(1).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly17).term(1).variable(1).hasNameThat().isEqualTo("z") - assertThat(poly17).term(1).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly17).term(2).hasVariableCountThat().isEqualTo(2) - assertThat(poly17).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly17).term(2).variable(0).hasNameThat().isEqualTo("y") - assertThat(poly17).term(2).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly17).term(2).variable(1).hasNameThat().isEqualTo("z") - assertThat(poly17).term(2).variable(1).hasPowerThat().isEqualTo(1) - - val poly18 = parseAlgebraicExpressionWithAllErrors("3+x+y+xy+x^2y+xy^2+x^2y^2").toPolynomial() - assertThat(poly18).evaluatesToPlainTextThat().isEqualTo("x^2y^2 + x^2y + xy^2 + xy + x + y + 3") - assertThat(poly18).hasTermCountThat().isEqualTo(7) - assertThat(poly18).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly18).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly18).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly18).term(3).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly18).term(4).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly18).term(5).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly18).term(6).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(3) - hasVariableCountThat().isEqualTo(0) - } - - // Ensure variables of coefficient and power of 0 are removed. - val poly22 = parseAlgebraicExpressionWithAllErrors("0x").toPolynomial() - assertThat(poly22).evaluatesToPlainTextThat().isEqualTo("0") - assertThat(poly22).hasTermCountThat().isEqualTo(1) - assertThat(poly22).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(0) - hasVariableCountThat().isEqualTo(0) - } - - val poly23 = parseAlgebraicExpressionWithAllErrors("x-x").toPolynomial() - assertThat(poly23).evaluatesToPlainTextThat().isEqualTo("0") - assertThat(poly23).hasTermCountThat().isEqualTo(1) - assertThat(poly23).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(0) - hasVariableCountThat().isEqualTo(0) - } - - val poly24 = parseAlgebraicExpressionWithAllErrors("x^0").toPolynomial() - assertThat(poly24).evaluatesToPlainTextThat().isEqualTo("1") - assertThat(poly24).hasTermCountThat().isEqualTo(1) - assertThat(poly24).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(0) - } - - val poly25 = parseAlgebraicExpressionWithAllErrors("x/x").toPolynomial() - assertThat(poly25).evaluatesToPlainTextThat().isEqualTo("1") - assertThat(poly25).hasTermCountThat().isEqualTo(1) - assertThat(poly25).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(0) - } - - val poly26 = parseAlgebraicExpressionWithAllErrors("x^(2-2)").toPolynomial() - assertThat(poly26).evaluatesToPlainTextThat().isEqualTo("1") - assertThat(poly26).hasTermCountThat().isEqualTo(1) - assertThat(poly26).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(0) - } - - val poly28 = parseAlgebraicExpressionWithAllErrors("(x+1)/2").toPolynomial() - assertThat(poly28).evaluatesToPlainTextThat().isEqualTo("(1/2)x + 1/2") - assertThat(poly28).hasTermCountThat().isEqualTo(2) - assertThat(poly28).term(0).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(2) - } - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly28).term(1).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(2) - } - hasVariableCountThat().isEqualTo(0) - } - - // Ensure like terms are combined after polynomial multiplication. - val poly20 = parseAlgebraicExpressionWithAllErrors("(x-5)(x+2)").toPolynomial() - assertThat(poly20).evaluatesToPlainTextThat().isEqualTo("x^2 - 3x - 10") - assertThat(poly20).hasTermCountThat().isEqualTo(3) - assertThat(poly20).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly20).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-3) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly20).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-10) - hasVariableCountThat().isEqualTo(0) - } - - val poly21 = parseAlgebraicExpressionWithAllErrors("(1+x)^3").toPolynomial() - assertThat(poly21).evaluatesToPlainTextThat().isEqualTo("x^3 + 3x^2 + 3x + 1") - assertThat(poly21).hasTermCountThat().isEqualTo(4) - assertThat(poly21).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(3) - } - } - assertThat(poly21).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(3) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly21).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(3) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly21).term(3).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(0) - } - - val poly27 = parseAlgebraicExpressionWithAllErrors("x^2*y^2 + 2").toPolynomial() - assertThat(poly27).evaluatesToPlainTextThat().isEqualTo("x^2y^2 + 2") - assertThat(poly27).hasTermCountThat().isEqualTo(2) - assertThat(poly27).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly27).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(2) - hasVariableCountThat().isEqualTo(0) - } - - val poly32 = parseAlgebraicExpressionWithAllErrors("(x^2-3*x-10)*(x+2)").toPolynomial() - assertThat(poly32).evaluatesToPlainTextThat().isEqualTo("x^3 - x^2 - 16x - 20") - assertThat(poly32).hasTermCountThat().isEqualTo(4) - assertThat(poly32).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(3) - } - } - assertThat(poly32).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly32).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-16) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly32).term(3).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-20) - hasVariableCountThat().isEqualTo(0) - } - - val poly33 = parseAlgebraicExpressionWithAllErrors("(x-y)^3").toPolynomial() - assertThat(poly33).evaluatesToPlainTextThat().isEqualTo("x^3 - 3x^2y + 3xy^2 - y^3") - assertThat(poly33).hasTermCountThat().isEqualTo(4) - assertThat(poly33).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(3) - } - } - assertThat(poly33).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-3) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly33).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(3) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly33).term(3).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(3) - } - } - - // Ensure polynomial division works. - val poly19 = parseAlgebraicExpressionWithAllErrors("(x^2-3*x-10)/(x+2)").toPolynomial() - assertThat(poly19).evaluatesToPlainTextThat().isEqualTo("x - 5") - assertThat(poly19).hasTermCountThat().isEqualTo(2) - assertThat(poly19).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly19).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-5) - hasVariableCountThat().isEqualTo(0) - } - - val poly35 = parseAlgebraicExpressionWithAllErrors("(xy-5y)/y").toPolynomial() - assertThat(poly35).evaluatesToPlainTextThat().isEqualTo("x - 5") - assertThat(poly35).hasTermCountThat().isEqualTo(2) - assertThat(poly35).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly35).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-5) - hasVariableCountThat().isEqualTo(0) - } - - val poly36 = parseAlgebraicExpressionWithAllErrors("(x^2-2xy+y^2)/(x-y)").toPolynomial() - assertThat(poly36).evaluatesToPlainTextThat().isEqualTo("x - y") - assertThat(poly36).hasTermCountThat().isEqualTo(2) - assertThat(poly36).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly36).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - - // Example from https://www.kristakingmath.com/blog/predator-prey-systems-ghtcp-5e2r4-427ab. - val poly37 = parseAlgebraicExpressionWithAllErrors("(x^3-y^3)/(x-y)").toPolynomial() - assertThat(poly37).evaluatesToPlainTextThat().isEqualTo("x^2 + xy + y^2") - assertThat(poly37).hasTermCountThat().isEqualTo(3) - assertThat(poly37).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly37).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly37).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(2) - } - } - - // Multi-variable & more complex division. - val poly34 = - parseAlgebraicExpressionWithAllErrors("(x^3-3x^2y+3xy^2-y^3)/(x-y)^2").toPolynomial() - assertThat(poly34).evaluatesToPlainTextThat().isEqualTo("x - y") - assertThat(poly34).hasTermCountThat().isEqualTo(2) - assertThat(poly34).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly34).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - - val poly38 = parseNumericExpressionWithAllErrors("2^-4").toPolynomial() - assertThat(poly38).evaluatesToPlainTextThat().isEqualTo("1/16") - assertThat(poly38).hasTermCountThat().isEqualTo(1) - assertThat(poly38).term(0).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(16) - } - hasVariableCountThat().isEqualTo(0) - } - - val poly39 = parseNumericExpressionWithAllErrors("2^(3-6)").toPolynomial() - assertThat(poly39).evaluatesToPlainTextThat().isEqualTo("1/8") - assertThat(poly39).hasTermCountThat().isEqualTo(1) - assertThat(poly39).term(0).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(8) - } - hasVariableCountThat().isEqualTo(0) - } - - // x^-3 is not a valid polynomial (since polynomials can't have negative powers). - val poly40 = parseAlgebraicExpressionWithAllErrors("x^(3-6)").toPolynomial() - assertThat(poly40).isNotValidPolynomial() - - // 2^x is not a polynomial. - val poly41 = parseAlgebraicExpressionWithoutOptionalErrors("2^x").toPolynomial() - assertThat(poly41).isNotValidPolynomial() - - // 1/x is not a polynomial. - val poly42 = parseAlgebraicExpressionWithoutOptionalErrors("1/x").toPolynomial() - assertThat(poly42).isNotValidPolynomial() - - val poly43 = parseAlgebraicExpressionWithAllErrors("x/2").toPolynomial() - assertThat(poly43).evaluatesToPlainTextThat().isEqualTo("(1/2)x") - assertThat(poly43).hasTermCountThat().isEqualTo(1) - assertThat(poly43).term(0).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(2) - } - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - - val poly44 = parseAlgebraicExpressionWithAllErrors("(x-3)/2").toPolynomial() - assertThat(poly44).evaluatesToPlainTextThat().isEqualTo("(1/2)x - 3/2") - assertThat(poly44).hasTermCountThat().isEqualTo(2) - assertThat(poly44).term(0).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(2) - } - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly44).term(1).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isTrue() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(3) - hasDenominatorThat().isEqualTo(2) - } - hasVariableCountThat().isEqualTo(0) - } - - val poly45 = parseAlgebraicExpressionWithAllErrors("(x-1)(x+1)").toPolynomial() - assertThat(poly45).evaluatesToPlainTextThat().isEqualTo("x^2 - 1") - assertThat(poly45).hasTermCountThat().isEqualTo(2) - assertThat(poly45).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly45).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-1) - hasVariableCountThat().isEqualTo(0) - } - - // √x is not a polynomial. - val poly46 = parseAlgebraicExpressionWithAllErrors("sqrt(x)").toPolynomial() - assertThat(poly46).isNotValidPolynomial() - - val poly47 = parseAlgebraicExpressionWithAllErrors("√(x^2)").toPolynomial() - assertThat(poly47).evaluatesToPlainTextThat().isEqualTo("x") - assertThat(poly47).hasTermCountThat().isEqualTo(1) - assertThat(poly47).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - - val poly51 = parseAlgebraicExpressionWithAllErrors("√(x^2y^2)").toPolynomial() - assertThat(poly51).evaluatesToPlainTextThat().isEqualTo("xy") - assertThat(poly51).hasTermCountThat().isEqualTo(1) - assertThat(poly51).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - - // A limitation in the current polynomial conversion is that sqrt(x) will fail due to it not - // have any polynomial representation. - val poly48 = parseAlgebraicExpressionWithAllErrors("√x^2").toPolynomial() - assertThat(poly48).isNotValidPolynomial() - - // √(x^2+2) may evaluate to a polynomial, but it requires factoring (which isn't yet supported). - val poly50 = parseAlgebraicExpressionWithAllErrors("√(x^2+2)").toPolynomial() - assertThat(poly50).isNotValidPolynomial() - - // Division by zero is undefined, so a polynomial can't be constructed. - val poly49 = parseAlgebraicExpressionWithoutOptionalErrors("(x+2)/0").toPolynomial() - assertThat(poly49).isNotValidPolynomial() - - val poly52 = parsePolynomialFromNumericExpression("1") - val poly53 = parsePolynomialFromNumericExpression("0") - assertThat(poly52).isNotEqualTo(poly53) - - val poly54 = parsePolynomialFromNumericExpression("1+2") - val poly55 = parsePolynomialFromNumericExpression("3") - assertThat(poly54).isEqualTo(poly55) - - val poly56 = parsePolynomialFromNumericExpression("1-2") - val poly57 = parsePolynomialFromNumericExpression("-1") - assertThat(poly56).isEqualTo(poly57) - - val poly58 = parsePolynomialFromNumericExpression("2*3") - val poly59 = parsePolynomialFromNumericExpression("6") - assertThat(poly58).isEqualTo(poly59) - - val poly60 = parsePolynomialFromNumericExpression("2^3") - val poly61 = parsePolynomialFromNumericExpression("8") - assertThat(poly60).isEqualTo(poly61) - - val poly62 = parsePolynomialFromAlgebraicExpression("1+x") - val poly63 = parsePolynomialFromAlgebraicExpression("x+1") - assertThat(poly62).isEqualTo(poly63) - - val poly64 = parsePolynomialFromAlgebraicExpression("y+x") - val poly65 = parsePolynomialFromAlgebraicExpression("x+y") - assertThat(poly64).isEqualTo(poly65) - - val poly66 = parsePolynomialFromAlgebraicExpression("(x+1)^2") - val poly67 = parsePolynomialFromAlgebraicExpression("x^2+2x+1") - assertThat(poly66).isEqualTo(poly67) - - val poly68 = parsePolynomialFromAlgebraicExpression("(x+1)/2") - val poly69 = parsePolynomialFromAlgebraicExpression("x/2+(1/2)") - assertThat(poly68).isEqualTo(poly69) - - val poly70 = parsePolynomialFromAlgebraicExpression("x*2") - val poly71 = parsePolynomialFromAlgebraicExpression("2x") - assertThat(poly70).isEqualTo(poly71) - - val poly72 = parsePolynomialFromAlgebraicExpression("x(x+1)") - val poly73 = parsePolynomialFromAlgebraicExpression("x^2+x") - assertThat(poly72).isEqualTo(poly73) - } - - private fun createComparableOperationListFromNumericExpression(expression: String) = - parseNumericExpressionWithAllErrors(expression).toComparableOperationList() - - private fun createComparableOperationListFromAlgebraicExpression(expression: String) = - parseAlgebraicExpressionWithAllErrors(expression).toComparableOperationList() - - private fun parsePolynomialFromNumericExpression(expression: String) = - parseNumericExpressionWithAllErrors(expression).toPolynomial() - - private fun parsePolynomialFromAlgebraicExpression(expression: String) = - parseAlgebraicExpressionWithAllErrors(expression).toPolynomial() - - @DslMarker private annotation class ExpressionComparatorMarker - - @DslMarker private annotation class ComparableOperationComparatorMarker - - // See: https://kotlinlang.org/docs/type-safe-builders.html. - private class MathExpressionSubject( - metadata: FailureMetadata, - private val actual: MathExpression - ) : LiteProtoSubject(metadata, actual) { - fun hasStructureThatMatches(init: ExpressionComparator.() -> Unit) { - // TODO: maybe verify that all aspects are verified? - ExpressionComparator.createFromExpression(actual).also(init) - } - - fun evaluatesToRationalThat(): FractionSubject = - assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.RATIONAL).rational) - - fun evaluatesToIrrationalThat(): DoubleSubject = - assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.IRRATIONAL).irrational) - - fun evaluatesToIntegerThat(): IntegerSubject = - assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.INTEGER).integer) - - fun convertsToLatexStringThat(): StringSubject = - assertThat(convertToLatex(divAsFraction = false)) - - fun convertsWithFractionsToLatexStringThat(): StringSubject = - assertThat(convertToLatex(divAsFraction = true)) - - fun forHumanReadable(language: OppiaLanguage): HumanReadableStringChecker = - HumanReadableStringChecker(language, actual::toHumanReadableString) - - private fun evaluateAsReal(expectedType: Real.RealTypeCase): Real { - val real = actual.evaluateAsNumericExpression() - assertWithMessage("Failed to evaluate numeric expression").that(real).isNotNull() - assertWithMessage("Expected constant to evaluate to $expectedType") - .that(real?.realTypeCase) - .isEqualTo(expectedType) - return checkNotNull(real) // Just to remove the nullable operator; the actual check is above. - } - - private fun convertToLatex(divAsFraction: Boolean): String = actual.toRawLatex(divAsFraction) - - // TODO: update DSL to not have return values (since it's unnecessary). - @ExpressionComparatorMarker - class ExpressionComparator private constructor(private val expression: MathExpression) { - // TODO: convert to constant comparator? - fun constant(init: ConstantComparator.() -> Unit): ConstantComparator = - ConstantComparator.createFromExpression(expression).also(init) - - fun variable(init: VariableComparator.() -> Unit): VariableComparator = - VariableComparator.createFromExpression(expression).also(init) - - fun addition(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( - expression, - expectedOperator = MathBinaryOperation.Operator.ADD - ).also(init) - } - - fun subtraction(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( - expression, - expectedOperator = MathBinaryOperation.Operator.SUBTRACT - ).also(init) - } - - fun multiplication(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( - expression, - expectedOperator = MathBinaryOperation.Operator.MULTIPLY - ).also(init) - } - - fun division(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( - expression, - expectedOperator = MathBinaryOperation.Operator.DIVIDE - ).also(init) - } - - fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( - expression, - expectedOperator = MathBinaryOperation.Operator.EXPONENTIATE - ).also(init) - } - - fun negation(init: UnaryOperationComparator.() -> Unit): UnaryOperationComparator { - return UnaryOperationComparator.createFromExpression( - expression, - expectedOperator = MathUnaryOperation.Operator.NEGATE - ).also(init) - } - - fun positive(init: UnaryOperationComparator.() -> Unit): UnaryOperationComparator { - return UnaryOperationComparator.createFromExpression( - expression, - expectedOperator = MathUnaryOperation.Operator.POSITIVE - ).also(init) - } - - fun functionCallTo( - type: MathFunctionCall.FunctionType, - init: FunctionCallComparator.() -> Unit - ): FunctionCallComparator { - return FunctionCallComparator.createFromExpression( - expression, - expectedFunctionType = type - ).also(init) - } - - fun group(init: ExpressionComparator.() -> Unit): ExpressionComparator { - return createFromExpression(expression.group).also(init) - } - - internal companion object { - fun createFromExpression(expression: MathExpression): ExpressionComparator = - ExpressionComparator(expression) - } - } - - @ExpressionComparatorMarker - class ConstantComparator private constructor(private val constant: Real) { - fun withValueThat(): RealSubject = assertThat(constant) - - internal companion object { - fun createFromExpression(expression: MathExpression): ConstantComparator { - assertThat(expression.expressionTypeCase).isEqualTo(CONSTANT) - return ConstantComparator(expression.constant) - } - } - } - - @ExpressionComparatorMarker - class VariableComparator private constructor(private val variableName: String) { - fun withNameThat(): StringSubject = assertThat(variableName) - - internal companion object { - fun createFromExpression(expression: MathExpression): VariableComparator { - assertThat(expression.expressionTypeCase).isEqualTo(VARIABLE) - return VariableComparator(expression.variable) - } - } - } - - @ExpressionComparatorMarker - class BinaryOperationComparator private constructor( - private val operation: MathBinaryOperation - ) { - fun leftOperand(init: ExpressionComparator.() -> Unit): ExpressionComparator = - ExpressionComparator.createFromExpression(operation.leftOperand).also(init) - - fun rightOperand(init: ExpressionComparator.() -> Unit): ExpressionComparator = - ExpressionComparator.createFromExpression(operation.rightOperand).also(init) - - internal companion object { - fun createFromExpression( - expression: MathExpression, - expectedOperator: MathBinaryOperation.Operator - ): BinaryOperationComparator { - assertThat(expression.expressionTypeCase).isEqualTo(BINARY_OPERATION) - assertWithMessage("Expected binary operation with operator: $expectedOperator") - .that(expression.binaryOperation.operator) - .isEqualTo(expectedOperator) - return BinaryOperationComparator(expression.binaryOperation) - } - } - } - - @ExpressionComparatorMarker - class UnaryOperationComparator private constructor( - private val operation: MathUnaryOperation - ) { - fun operand(init: ExpressionComparator.() -> Unit): ExpressionComparator = - ExpressionComparator.createFromExpression(operation.operand).also(init) - - internal companion object { - fun createFromExpression( - expression: MathExpression, - expectedOperator: MathUnaryOperation.Operator - ): UnaryOperationComparator { - assertThat(expression.expressionTypeCase).isEqualTo(UNARY_OPERATION) - assertWithMessage("Expected unary operation with operator: $expectedOperator") - .that(expression.unaryOperation.operator) - .isEqualTo(expectedOperator) - return UnaryOperationComparator(expression.unaryOperation) - } - } - } - - @ExpressionComparatorMarker - class FunctionCallComparator private constructor( - private val functionCall: MathFunctionCall - ) { - fun argument(init: ExpressionComparator.() -> Unit): ExpressionComparator = - ExpressionComparator.createFromExpression(functionCall.argument).also(init) - - internal companion object { - fun createFromExpression( - expression: MathExpression, - expectedFunctionType: MathFunctionCall.FunctionType - ): FunctionCallComparator { - assertThat(expression.expressionTypeCase).isEqualTo(FUNCTION_CALL) - assertWithMessage("Expected function call to: $expectedFunctionType") - .that(expression.functionCall.functionType) - .isEqualTo(expectedFunctionType) - return FunctionCallComparator(expression.functionCall) - } - } - } - } - - private class MathEquationSubject( - metadata: FailureMetadata, - private val actual: MathEquation - ) : LiteProtoSubject(metadata, actual) { - fun hasLeftHandSideThat(): MathExpressionSubject = assertThat(actual.leftSide) - - fun hasRightHandSideThat(): MathExpressionSubject = assertThat(actual.rightSide) - - fun convertsToLatexStringThat(): StringSubject = - assertThat(convertToLatex(divAsFraction = false)) - - fun convertsWithFractionsToLatexStringThat(): StringSubject = - assertThat(convertToLatex(divAsFraction = true)) - - fun forHumanReadable(language: OppiaLanguage): HumanReadableStringChecker = - HumanReadableStringChecker(language, actual::toHumanReadableString) - - private fun convertToLatex(divAsFraction: Boolean): String = actual.toRawLatex(divAsFraction) - } - - private class HumanReadableStringChecker( - private val language: OppiaLanguage, - private val maybeConvertToHumanReadableString: (OppiaLanguage, Boolean) -> String? - ) { - fun convertsToStringThat(): StringSubject = - assertThat(convertToHumanReadableString(language, /* divAsFraction= */ false)) - - fun convertsWithFractionsToStringThat(): StringSubject = - assertThat(convertToHumanReadableString(language, /* divAsFraction= */ true)) - - fun doesNotConvertToString() { - assertWithMessage("Expected to not convert to: $language") - .that(maybeConvertToHumanReadableString(language, /* divAsFraction= */ false)) - .isNull() - } - - private fun convertToHumanReadableString( - language: OppiaLanguage, divAsFraction: Boolean - ): String { - val readableString = maybeConvertToHumanReadableString(language, divAsFraction) - assertWithMessage("Expected to convert to: $language").that(readableString).isNotNull() - return checkNotNull(readableString) // Verified in the above assertion check. - } - } - - // TODO: move these to a common location. - private class FractionSubject( - metadata: FailureMetadata, - private val actual: Fraction - ) : LiteProtoSubject(metadata, actual) { - fun hasNegativePropertyThat(): BooleanSubject = assertThat(actual.isNegative) - - fun hasWholeNumberThat(): IntegerSubject = assertThat(actual.wholeNumber) - - fun hasNumeratorThat(): IntegerSubject = assertThat(actual.numerator) - - fun hasDenominatorThat(): IntegerSubject = assertThat(actual.denominator) - - fun evaluatesToRealThat(): DoubleSubject = assertThat(actual.toDouble()) - } - - private class RealSubject( - metadata: FailureMetadata, - private val actual: Real - ) : LiteProtoSubject(metadata, actual) { - fun isRationalThat(): FractionSubject { - verifyTypeToBe(Real.RealTypeCase.RATIONAL) - return assertThat(actual.rational) - } - - fun isIrrationalThat(): DoubleSubject { - verifyTypeToBe(Real.RealTypeCase.IRRATIONAL) - return assertThat(actual.irrational) - } - - fun isIntegerThat(): IntegerSubject { - verifyTypeToBe(Real.RealTypeCase.INTEGER) - return assertThat(actual.integer) - } - - private fun verifyTypeToBe(expected: Real.RealTypeCase) { - assertWithMessage("Expected real type to be $expected, not: ${actual.realTypeCase}") - .that(actual.realTypeCase) - .isEqualTo(expected) - } - } - - private class ComparableOperationListSubject( - metadata: FailureMetadata, - private val actual: ComparableOperationList - ) : LiteProtoSubject(metadata, actual) { - fun hasStructureThatMatches(init: ComparableOperationComparator.() -> Unit) { - ComparableOperationComparator.createFrom(actual.rootOperation).also(init) - } - - @ComparableOperationComparatorMarker - class ComparableOperationComparator private constructor( - private val operation: ComparableOperation - ) { - fun hasNegatedPropertyThat(): BooleanSubject = assertThat(operation.isNegated) - - fun hasInvertedPropertyThat(): BooleanSubject = assertThat(operation.isInverted) - - fun commutativeAccumulationWithType( - type: ComparableOperationList.CommutativeAccumulation.AccumulationType, - init: CommutativeAccumulationComparator.() -> Unit - ): CommutativeAccumulationComparator = - CommutativeAccumulationComparator.createFrom(type, operation).also(init) - - fun nonCommutativeOperation( - init: NonCommutativeOperationComparator.() -> Unit - ): NonCommutativeOperationComparator = - NonCommutativeOperationComparator.createFrom(operation).also(init) - - fun constantTerm(init: ConstantTermComparator.() -> Unit): ConstantTermComparator = - ConstantTermComparator.createFrom(operation).also(init) - - fun variableTerm(init: VariableTermComparator.() -> Unit): VariableTermComparator = - VariableTermComparator.createFrom(operation).also(init) - - internal companion object { - fun createFrom(operation: ComparableOperation): ComparableOperationComparator = - ComparableOperationComparator(operation) - } - } - - @ComparableOperationComparatorMarker - class CommutativeAccumulationComparator private constructor( - private val accumulation: ComparableOperationList.CommutativeAccumulation - ) { - fun hasOperandCountThat(): IntegerSubject = assertThat(accumulation.combinedOperationsCount) - - fun index( - index: Int, - init: ComparableOperationComparator.() -> Unit - ): ComparableOperationComparator { - return ComparableOperationComparator.createFrom( - accumulation.combinedOperationsList[index] - ).also(init) - } - - internal companion object { - fun createFrom( - type: ComparableOperationList.CommutativeAccumulation.AccumulationType, - operation: ComparableOperation - ): CommutativeAccumulationComparator { - assertThat(operation.comparisonTypeCase) - .isEqualTo(ComparisonTypeCase.COMMUTATIVE_ACCUMULATION) - assertThat(operation.commutativeAccumulation.accumulationType).isEqualTo(type) - return CommutativeAccumulationComparator(operation.commutativeAccumulation) - } - } - } - - @ComparableOperationComparatorMarker - class NonCommutativeOperationComparator private constructor( - private val operation: ComparableOperationList.NonCommutativeOperation - ) { - fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - verifyTypeAs( - ComparableOperationList.NonCommutativeOperation.OperationTypeCase.EXPONENTIATION - ) - return BinaryOperationComparator.createFrom(operation.exponentiation).also(init) - } - - fun squareRootWithArgument( - init: ComparableOperationComparator.() -> Unit - ): ComparableOperationComparator { - verifyTypeAs(ComparableOperationList.NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT) - return ComparableOperationComparator.createFrom(operation.squareRoot).also(init) - } - - private fun verifyTypeAs( - type: ComparableOperationList.NonCommutativeOperation.OperationTypeCase - ) { - assertThat(operation.operationTypeCase).isEqualTo(type) - } - - internal companion object { - fun createFrom(operation: ComparableOperation): NonCommutativeOperationComparator { - assertThat(operation.comparisonTypeCase) - .isEqualTo(ComparisonTypeCase.NON_COMMUTATIVE_OPERATION) - return NonCommutativeOperationComparator(operation.nonCommutativeOperation) - } - } - } - - @ComparableOperationComparatorMarker - class BinaryOperationComparator private constructor( - private val operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation - ) { - fun leftOperand( - init: ComparableOperationComparator.() -> Unit - ): ComparableOperationComparator = - ComparableOperationComparator.createFrom(operation.leftOperand).also(init) - - fun rightOperand( - init: ComparableOperationComparator.() -> Unit - ): ComparableOperationComparator = - ComparableOperationComparator.createFrom(operation.rightOperand).also(init) - - internal companion object { - fun createFrom( - operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation - ): BinaryOperationComparator = BinaryOperationComparator(operation) - } - } - - @ComparableOperationComparatorMarker - class ConstantTermComparator private constructor( - private val constant: Real - ) { - fun withValueThat(): RealSubject = assertThat(constant) - - internal companion object { - fun createFrom(operation: ComparableOperation): ConstantTermComparator { - assertThat(operation.comparisonTypeCase).isEqualTo(ComparisonTypeCase.CONSTANT_TERM) - return ConstantTermComparator(operation.constantTerm) - } - } - } - - @ComparableOperationComparatorMarker - class VariableTermComparator private constructor( - private val variableName: String - ) { - fun withNameThat(): StringSubject = assertThat(variableName) - - internal companion object { - fun createFrom(operation: ComparableOperation): VariableTermComparator { - assertThat(operation.comparisonTypeCase).isEqualTo(ComparisonTypeCase.VARIABLE_TERM) - return VariableTermComparator(operation.variableTerm) - } - } - } - } - - private class PolynomialSubject( - metadata: FailureMetadata, - private val actual: Polynomial? - ) : LiteProtoSubject(metadata, actual) { - private val nonNullActual by lazy { - checkNotNull(actual) { - "Expected polynomial to be defined, not null (is the expression/equation not a valid" + - " polynomial?)" - } - } - - fun isNotValidPolynomial() { - assertWithMessage( - "Expected polynomial to be undefined, but was: ${actual?.toPlainText()}" - ).that(actual).isNull() - } - - fun isConstantThat(): RealSubject { - assertWithMessage("Expected polynomial to be constant: $nonNullActual") - .that(nonNullActual.isConstant()) - .isTrue() - return assertThat(nonNullActual.getConstant()) - } - - fun hasTermCountThat(): IntegerSubject = assertThat(nonNullActual.termCount) - - fun term(index: Int): PolynomialTermSubject = assertThat(nonNullActual.termList[index]) - - fun evaluatesToPlainTextThat(): StringSubject = assertThat(nonNullActual.toPlainText()) - } - - private class PolynomialTermSubject( - metadata: FailureMetadata, - private val actual: Polynomial.Term - ) : LiteProtoSubject(metadata, actual) { - fun hasCoefficientThat(): RealSubject = assertThat(actual.coefficient) - - fun hasVariableCountThat(): IntegerSubject = assertThat(actual.variableCount) - - fun variable(index: Int): PolynomialTermVariableSubject = - assertThat(actual.variableList[index]) - } - - private class PolynomialTermVariableSubject( - metadata: FailureMetadata, - private val actual: Polynomial.Term.Variable - ) : LiteProtoSubject(metadata, actual) { - fun hasNameThat(): StringSubject = assertThat(actual.name) - - fun hasPowerThat(): IntegerSubject = assertThat(actual.power) - } - - private companion object { - // TODO: fix helper API. - - private fun expectFailureWhenParsingNumericExpression(expression: String): MathParsingError { - val result = parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) - assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) - return (result as MathParsingResult.Failure).error - } - - private fun parseNumericExpressionWithoutOptionalErrors(expression: String): MathExpression { - return (parseNumericExpressionInternal(expression, ErrorCheckingMode.REQUIRED_ONLY) as MathParsingResult.Success).result - } - - private fun parseNumericExpressionWithAllErrors(expression: String): MathExpression { - return (parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) as MathParsingResult.Success).result - } - - private fun parseNumericExpressionInternal( - expression: String, errorCheckingMode: ErrorCheckingMode - ): MathParsingResult { - return NumericExpressionParser.parseNumericExpression(expression, errorCheckingMode) - } - - private fun expectFailureWhenParsingAlgebraicExpression( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingError { - val result = - parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) - assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) - return (result as MathParsingResult.Failure).error - } - - private fun parseAlgebraicExpressionWithoutOptionalErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathExpression { - return (parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables) as MathParsingResult.Success).result - } - - private fun parseAlgebraicExpressionWithAllErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathExpression { - return (parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) as MathParsingResult.Success).result - } - - private fun parseAlgebraicExpressionInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingResult { - return NumericExpressionParser.parseAlgebraicExpression( - expression, allowedVariables, errorCheckingMode - ) - } - - private fun expectFailureWhenParsingAlgebraicEquation(expression: String): MathParsingError { - val result = parseAlgebraicEquationInternal(expression, ErrorCheckingMode.ALL_ERRORS) - assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) - return (result as MathParsingResult.Failure).error - } - - private fun parseAlgebraicEquationWithAllErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathEquation { - return (parseAlgebraicEquationInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) as MathParsingResult.Success).result - } - - private fun parseAlgebraicEquationInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingResult { - return NumericExpressionParser.parseAlgebraicEquation( - expression, allowedVariables, errorCheckingMode - ) - } - - private fun assertThat(actual: MathExpression): MathExpressionSubject = - assertAbout(::MathExpressionSubject).that(actual) - - private fun assertThat(actual: MathEquation): MathEquationSubject = - assertAbout(::MathEquationSubject).that(actual) - - private fun assertThat(actual: Fraction): FractionSubject = - assertAbout(::FractionSubject).that(actual) - - private fun assertThat(actual: Real): RealSubject = assertAbout(::RealSubject).that(actual) - - private fun assertThat(actual: ComparableOperationList): ComparableOperationListSubject = - assertAbout(::ComparableOperationListSubject).that(actual) - - private fun assertThat(actual: Polynomial?): PolynomialSubject = - assertAbout(::PolynomialSubject).that(actual) - - private fun assertThat(actual: Polynomial.Term): PolynomialTermSubject = - assertAbout(::PolynomialTermSubject).that(actual) - - private fun assertThat(actual: Polynomial.Term.Variable): PolynomialTermVariableSubject = - assertAbout(::PolynomialTermVariableSubject).that(actual) - } -} From 34525f42266693791fc287918dfe940790c5f17d Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 13 Dec 2021 19:08:00 -0800 Subject: [PATCH 090/289] Lint. --- .../util/math/MathExpressionExtensions.kt | 97 +++++++++++-------- .../android/util/math/MathExpressionParser.kt | 73 +++++++------- .../android/util/math/MathParsingError.kt | 25 ++--- .../oppia/android/util/math/MathTokenizer.kt | 54 ++++++++--- .../util/math/MathExpressionParserTest.kt | 68 +++++++++---- 5 files changed, 192 insertions(+), 125 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 147e074cd94..3708f392ae6 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -1,19 +1,39 @@ package org.oppia.android.util.math -import java.text.NumberFormat -import java.util.Locale -import java.util.SortedSet +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.COMPARISONTYPE_NOT_SET +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.CONSTANT_TERM +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.VARIABLE_TERM +import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation +import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation.OperationTypeCase import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathFunctionCall.FunctionType import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.OppiaLanguage +import org.oppia.android.app.model.OppiaLanguage.ARABIC +import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.ENGLISH +import org.oppia.android.app.model.OppiaLanguage.HINDI +import org.oppia.android.app.model.OppiaLanguage.HINGLISH +import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED +import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Polynomial.Term import org.oppia.android.app.model.Polynomial.Term.Variable @@ -22,34 +42,14 @@ import org.oppia.android.app.model.Real.RealTypeCase.INTEGER import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET +import java.text.NumberFormat +import java.util.Locale +import java.util.SortedSet import kotlin.math.abs import kotlin.math.pow import kotlin.math.sqrt -import org.oppia.android.app.model.ComparableOperationList -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.COMPARISONTYPE_NOT_SET -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.CONSTANT_TERM -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.VARIABLE_TERM -import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation -import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation.OperationTypeCase import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator -import org.oppia.android.app.model.MathEquation -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP -import org.oppia.android.app.model.MathFunctionCall.FunctionType import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator -import org.oppia.android.app.model.OppiaLanguage -import org.oppia.android.app.model.OppiaLanguage.ARABIC -import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE -import org.oppia.android.app.model.OppiaLanguage.ENGLISH -import org.oppia.android.app.model.OppiaLanguage.HINDI -import org.oppia.android.app.model.OppiaLanguage.HINGLISH -import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED -import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE -import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED // TODO: split up this extensions file into multiple, clean it up, reorganize, and add tests. @@ -74,9 +74,12 @@ private val COMPARABLE_OPERATION_COMPARATOR: Comparator by private val COMMUTATIVE_ACCUMULATION_COMPARATOR: Comparator by lazy { Comparator.comparing(CommutativeAccumulation::getAccumulationType) - .thenComparing({ accumulation -> - accumulation.combinedOperationsList.toSortedSet(COMPARABLE_OPERATION_COMPARATOR) - }, COMPARABLE_OPERATION_COMPARATOR.toSetComparator()) + .thenComparing( + { accumulation -> + accumulation.combinedOperationsList.toSortedSet(COMPARABLE_OPERATION_COMPARATOR) + }, + COMPARABLE_OPERATION_COMPARATOR.toSetComparator() + ) } private val NON_COMMUTATIVE_BINARY_OPERATION_COMPARATOR by lazy { @@ -126,7 +129,8 @@ private val POLYNOMIAL_TERM_COMPARATOR: Comparator by lazy { } private fun comparingDeferred( - keySelector: (T) -> U, comparatorSelector: () -> Comparator + keySelector: (T) -> U, + comparatorSelector: () -> Comparator ): Comparator { // Store as captured val for memoization. val comparator by lazy { comparatorSelector() } @@ -135,12 +139,13 @@ private fun comparingDeferred( } } -private fun > Comparator.thenComparingReversed( +private fun > Comparator.thenComparingReversed( keySelector: (T) -> U ): Comparator = thenComparing(Comparator.comparing(keySelector).reversed()) -private fun > Comparator.thenSelectAmong( - enumSelector: (T) -> E, vararg comparators: Pair> +private fun > Comparator.thenSelectAmong( + enumSelector: (T) -> E, + vararg comparators: Pair> ): Comparator { val comparatorMap = comparators.toMap() return thenComparing( @@ -383,7 +388,8 @@ private fun MathExpression.toProduct(isRhsInverted: Boolean): ComparableOperatio } private fun CommutativeAccumulation.Builder.addOperationToSum( - expression: MathExpression, forceNegative: Boolean + expression: MathExpression, + forceNegative: Boolean ) { when (expression.binaryOperation.operator) { BinaryOperator.ADD -> { @@ -402,7 +408,8 @@ private fun CommutativeAccumulation.Builder.addOperationToSum( } private fun CommutativeAccumulation.Builder.addOperationToProduct( - expression: MathExpression, forceInverse: Boolean + expression: MathExpression, + forceInverse: Boolean ) { when (expression.binaryOperation.operator) { BinaryOperator.MULTIPLY -> { @@ -945,9 +952,11 @@ private fun Polynomial.combineLikeTerms(): Polynomial { private fun Polynomial.removeUnnecessaryVariables(): Polynomial { return Polynomial.newBuilder().apply { - addAllTerm(this@removeUnnecessaryVariables.termList.filter { term -> - !term.coefficient.isApproximatelyZero() - }) + addAllTerm( + this@removeUnnecessaryVariables.termList.filter { term -> + !term.coefficient.isApproximatelyZero() + } + ) }.build().ensureAtLeastConstant() } @@ -1069,10 +1078,12 @@ private fun createSingleVariablePolynomial(variableName: String): Polynomial { return createSingleTermPolynomial( Term.newBuilder().apply { coefficient = createCoefficientValueOfOne() - addVariable(Variable.newBuilder().apply { - name = variableName - power = 1 - }.build()) + addVariable( + Variable.newBuilder().apply { + name = variableName + power = 1 + }.build() + ) }.build() ) } @@ -1282,7 +1293,7 @@ private fun Real.isNegative(): Boolean = when (realTypeCase) { RATIONAL -> rational.isNegative IRRATIONAL -> irrational < 0 INTEGER -> integer < 0 - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") } private fun Real.asWholeNumber(): Int? { diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index 90447df7532..80b0acd49b3 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -1,6 +1,5 @@ package org.oppia.android.util.math -import kotlin.math.absoluteValue import org.oppia.android.app.model.MathBinaryOperation import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE @@ -21,6 +20,8 @@ import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE import org.oppia.android.app.model.Real +import org.oppia.android.util.math.MathExpressionParser.ParseContext.AlgebraicExpressionContext +import org.oppia.android.util.math.MathExpressionParser.ParseContext.NumericExpressionContext import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError @@ -44,6 +45,7 @@ import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError +import org.oppia.android.util.math.MathTokenizer.Companion.BinaryOperatorToken import org.oppia.android.util.math.MathTokenizer.Companion.Token import org.oppia.android.util.math.MathTokenizer.Companion.Token.DivideSymbol import org.oppia.android.util.math.MathTokenizer.Companion.Token.EqualsSymbol @@ -60,9 +62,7 @@ import org.oppia.android.util.math.MathTokenizer.Companion.Token.PositiveRealNum import org.oppia.android.util.math.MathTokenizer.Companion.Token.RightParenthesisSymbol import org.oppia.android.util.math.MathTokenizer.Companion.Token.SquareRootSymbol import org.oppia.android.util.math.MathTokenizer.Companion.Token.VariableName -import org.oppia.android.util.math.MathExpressionParser.ParseContext.AlgebraicExpressionContext -import org.oppia.android.util.math.MathExpressionParser.ParseContext.NumericExpressionContext -import org.oppia.android.util.math.MathTokenizer.Companion.BinaryOperatorToken +import kotlin.math.absoluteValue class MathExpressionParser private constructor(private val parseContext: ParseContext) { // TODO: @@ -286,8 +286,8 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo return when (val nextToken = parseContext.peekToken()) { is MinusSymbol, is PlusSymbol -> parseGenericPlusMinusUnaryTerm() is PositiveInteger, is PositiveRealNumber -> parseNumber().takeUnless { - parseContext.hasNextTokenOfType() - || parseContext.hasNextTokenOfType() + parseContext.hasNextTokenOfType() || + parseContext.hasNextTokenOfType() } ?: SpacesBetweenNumbersError.toFailure() is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> parseGenericTermWithoutUnaryWithoutNumber() @@ -536,15 +536,16 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } private fun parseGenericBinaryExpression( - parseLhs: () -> MathParsingResult, parseRhs: (Token?) -> BinaryOperationRhs? + parseLhs: () -> MathParsingResult, + parseRhs: (Token?) -> BinaryOperationRhs? ): MathParsingResult { var lastLhsResult = parseLhs() while (!lastLhsResult.isFailure()) { // Compute the next LHS if there are further RHS expressions. lastLhsResult = parseRhs(parseContext.peekToken()) - ?.computeBinaryOperationExpression(lastLhsResult) - ?: break // Not a match to the expression. + ?.computeBinaryOperationExpression(lastLhsResult) + ?: break // Not a match to the expression. } return lastLhsResult } @@ -664,7 +665,8 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } class NumericExpressionContext( - rawExpression: String, override val errorCheckingMode: ErrorCheckingMode + rawExpression: String, + override val errorCheckingMode: ErrorCheckingMode ) : ParseContext(rawExpression) { // Numeric expressions never allow variables. override fun allowsVariables(): Boolean = false @@ -695,7 +697,8 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } fun parseNumericExpression( - rawExpression: String, errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS + rawExpression: String, + errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS ): MathParsingResult = createNumericParser(rawExpression, errorCheckingMode).parseGenericExpressionGrammar() @@ -720,7 +723,8 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } private fun createNumericParser( - rawExpression: String, errorCheckingMode: ErrorCheckingMode + rawExpression: String, + errorCheckingMode: ErrorCheckingMode ): MathExpressionParser = MathExpressionParser(NumericExpressionContext(rawExpression, errorCheckingMode)) @@ -883,8 +887,9 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } UNARY_OPERATION -> unaryOperation.operand.findFirstMultiRedundantGroup() FUNCTION_CALL -> functionCall.argument.findFirstMultiRedundantGroup() - GROUP -> group.takeIf { it.expressionTypeCase == GROUP } - ?: group.findFirstMultiRedundantGroup() + GROUP -> + group.takeIf { it.expressionTypeCase == GROUP } + ?: group.findFirstMultiRedundantGroup() CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null } } @@ -923,10 +928,10 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo return when (expressionTypeCase) { BINARY_OPERATION -> { takeIf { - binaryOperation.operator == EXPONENTIATE - && binaryOperation.rightOperand.isVariableExpression() + binaryOperation.operator == EXPONENTIATE && + binaryOperation.rightOperand.isVariableExpression() } ?: binaryOperation.leftOperand.findNextExponentiationWithVariablePower() - ?: binaryOperation.rightOperand.findNextExponentiationWithVariablePower() + ?: binaryOperation.rightOperand.findNextExponentiationWithVariablePower() } UNARY_OPERATION -> unaryOperation.operand.findNextExponentiationWithVariablePower() FUNCTION_CALL -> functionCall.argument.findNextExponentiationWithVariablePower() @@ -939,11 +944,11 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo return when (expressionTypeCase) { BINARY_OPERATION -> { takeIf { - binaryOperation.operator == EXPONENTIATE - && binaryOperation.rightOperand.expressionTypeCase == CONSTANT - && binaryOperation.rightOperand.constant.toDouble() > 5.0 + binaryOperation.operator == EXPONENTIATE && + binaryOperation.rightOperand.expressionTypeCase == CONSTANT && + binaryOperation.rightOperand.constant.toDouble() > 5.0 } ?: binaryOperation.leftOperand.findNextExponentiationWithTooLargePower() - ?: binaryOperation.rightOperand.findNextExponentiationWithTooLargePower() + ?: binaryOperation.rightOperand.findNextExponentiationWithTooLargePower() } UNARY_OPERATION -> unaryOperation.operand.findNextExponentiationWithTooLargePower() FUNCTION_CALL -> functionCall.argument.findNextExponentiationWithTooLargePower() @@ -956,10 +961,10 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo return when (expressionTypeCase) { BINARY_OPERATION -> { takeIf { - binaryOperation.operator == EXPONENTIATE - && binaryOperation.rightOperand.containsExponentiation() + binaryOperation.operator == EXPONENTIATE && + binaryOperation.rightOperand.containsExponentiation() } ?: binaryOperation.leftOperand.findNextNestedExponentiation() - ?: binaryOperation.rightOperand.findNextNestedExponentiation() + ?: binaryOperation.rightOperand.findNextNestedExponentiation() } UNARY_OPERATION -> unaryOperation.operand.findNextNestedExponentiation() FUNCTION_CALL -> functionCall.argument.findNextNestedExponentiation() @@ -972,12 +977,12 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo return when (expressionTypeCase) { BINARY_OPERATION -> { takeIf { - binaryOperation.operator == DIVIDE - && binaryOperation.rightOperand.expressionTypeCase == CONSTANT - && binaryOperation.rightOperand.constant - .toDouble().absoluteValue.approximatelyEquals(0.0) + binaryOperation.operator == DIVIDE && + binaryOperation.rightOperand.expressionTypeCase == CONSTANT && + binaryOperation.rightOperand.constant + .toDouble().absoluteValue.approximatelyEquals(0.0) } ?: binaryOperation.leftOperand.findNextDivisionByZero() - ?: binaryOperation.rightOperand.findNextDivisionByZero() + ?: binaryOperation.rightOperand.findNextDivisionByZero() } UNARY_OPERATION -> unaryOperation.operand.findNextDivisionByZero() FUNCTION_CALL -> functionCall.argument.findNextDivisionByZero() @@ -1012,8 +1017,8 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo return when (expressionTypeCase) { VARIABLE -> true BINARY_OPERATION -> { - binaryOperation.leftOperand.isVariableExpression() - || binaryOperation.rightOperand.isVariableExpression() + binaryOperation.leftOperand.isVariableExpression() || + binaryOperation.rightOperand.isVariableExpression() } UNARY_OPERATION -> unaryOperation.operand.isVariableExpression() FUNCTION_CALL -> functionCall.argument.isVariableExpression() @@ -1025,9 +1030,9 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo private fun MathExpression.containsExponentiation(): Boolean { return when (expressionTypeCase) { BINARY_OPERATION -> { - binaryOperation.operator == EXPONENTIATE - || binaryOperation.leftOperand.containsExponentiation() - || binaryOperation.rightOperand.containsExponentiation() + binaryOperation.operator == EXPONENTIATE || + binaryOperation.leftOperand.containsExponentiation() || + binaryOperation.rightOperand.containsExponentiation() } UNARY_OPERATION -> unaryOperation.operand.containsExponentiation() FUNCTION_CALL -> functionCall.argument.containsExponentiation() diff --git a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt index 24c1c28d242..44fd1debb4a 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt @@ -10,35 +10,38 @@ sealed class MathParsingError { object UnbalancedParenthesesError : MathParsingError() data class SingleRedundantParenthesesError( - val rawExpression: String, val expression: MathExpression - ): MathParsingError() + val rawExpression: String, + val expression: MathExpression + ) : MathParsingError() data class MultipleRedundantParenthesesError( - val rawExpression: String, val expression: MathExpression - ): MathParsingError() + val rawExpression: String, + val expression: MathExpression + ) : MathParsingError() data class RedundantParenthesesForIndividualTermsError( - val rawExpression: String, val expression: MathExpression - ): MathParsingError() + val rawExpression: String, + val expression: MathExpression + ) : MathParsingError() - data class UnnecessarySymbolsError(val invalidSymbol: String): MathParsingError() + data class UnnecessarySymbolsError(val invalidSymbol: String) : MathParsingError() - data class NumberAfterVariableError(val number: Real, val variable: String): MathParsingError() + data class NumberAfterVariableError(val number: Real, val variable: String) : MathParsingError() data class SubsequentBinaryOperatorsError( val operator1: String, val operator2: String - ): MathParsingError() + ) : MathParsingError() object SubsequentUnaryOperatorsError : MathParsingError() data class NoVariableOrNumberBeforeBinaryOperatorError( val operator: MathBinaryOperation.Operator - ): MathParsingError() + ) : MathParsingError() data class NoVariableOrNumberAfterBinaryOperatorError( val operator: MathBinaryOperation.Operator - ): MathParsingError() + ) : MathParsingError() object ExponentIsVariableExpressionError : MathParsingError() diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt index 47e1b8f2d76..37ca0410cd0 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt @@ -1,6 +1,5 @@ package org.oppia.android.util.math -import java.lang.StringBuilder import org.oppia.android.app.model.MathBinaryOperation import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE @@ -10,6 +9,7 @@ import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import java.lang.StringBuilder // TODO: rename to MathTokenizer & add documentation. // TODO: consider a more efficient implementation that uses 1 underlying buffer (which could still @@ -109,7 +109,9 @@ class MathTokenizer private constructor() { } private fun tokenizeFunctionName( - currChar: Char, startIndex: Int, chars: PeekableIterator + currChar: Char, + startIndex: Int, + chars: PeekableIterator ): Token? { // allowed_function_name = "sqrt" ; // disallowed_function_name = @@ -220,7 +222,10 @@ class MathTokenizer private constructor() { } private fun tokenizeExpectedFunction( - name: String, isAllowedFunction: Boolean, startIndex: Int, chars: PeekableIterator + name: String, + isAllowedFunction: Boolean, + startIndex: Int, + chars: PeekableIterator ): Token { return chars.expectNextCharsForFunctionName(name.substring(1), startIndex) ?: Token.FunctionName( @@ -261,24 +266,33 @@ class MathTokenizer private constructor() { abstract val endIndex: Int class PositiveInteger( - val parsedValue: Int, override val startIndex: Int, override val endIndex: Int + val parsedValue: Int, + override val startIndex: Int, + override val endIndex: Int ) : Token() class PositiveRealNumber( - val parsedValue: Double, override val startIndex: Int, override val endIndex: Int + val parsedValue: Double, + override val startIndex: Int, + override val endIndex: Int ) : Token() class VariableName( - val parsedName: String, override val startIndex: Int, override val endIndex: Int + val parsedName: String, + override val startIndex: Int, + override val endIndex: Int ) : Token() class FunctionName( - val parsedName: String, val isAllowedFunction: Boolean, override val startIndex: Int, + val parsedName: String, + val isAllowedFunction: Boolean, + override val startIndex: Int, override val endIndex: Int ) : Token() class MinusSymbol( - override val startIndex: Int, override val endIndex: Int + override val startIndex: Int, + override val endIndex: Int ) : Token(), UnaryOperatorToken, BinaryOperatorToken { override fun getUnaryOperator(): MathUnaryOperation.Operator = NEGATE @@ -288,7 +302,8 @@ class MathTokenizer private constructor() { class SquareRootSymbol(override val startIndex: Int, override val endIndex: Int) : Token() class PlusSymbol( - override val startIndex: Int, override val endIndex: Int + override val startIndex: Int, + override val endIndex: Int ) : Token(), UnaryOperatorToken, BinaryOperatorToken { override fun getUnaryOperator(): MathUnaryOperation.Operator = POSITIVE @@ -296,19 +311,22 @@ class MathTokenizer private constructor() { } class MultiplySymbol( - override val startIndex: Int, override val endIndex: Int + override val startIndex: Int, + override val endIndex: Int ) : Token(), BinaryOperatorToken { override fun getBinaryOperator(): MathBinaryOperation.Operator = MULTIPLY } class DivideSymbol( - override val startIndex: Int, override val endIndex: Int + override val startIndex: Int, + override val endIndex: Int ) : Token(), BinaryOperatorToken { override fun getBinaryOperator(): MathBinaryOperation.Operator = DIVIDE } class ExponentiationSymbol( - override val startIndex: Int, override val endIndex: Int + override val startIndex: Int, + override val endIndex: Int ) : Token(), BinaryOperatorToken { override fun getBinaryOperator(): MathBinaryOperation.Operator = EXPONENTIATE } @@ -316,15 +334,18 @@ class MathTokenizer private constructor() { class EqualsSymbol(override val startIndex: Int, override val endIndex: Int) : Token() class LeftParenthesisSymbol( - override val startIndex: Int, override val endIndex: Int + override val startIndex: Int, + override val endIndex: Int ) : Token() class RightParenthesisSymbol( - override val startIndex: Int, override val endIndex: Int + override val startIndex: Int, + override val endIndex: Int ) : Token() class IncompleteFunctionName( - override val startIndex: Int, override val endIndex: Int + override val startIndex: Int, + override val endIndex: Int ) : Token() class InvalidToken(override val startIndex: Int, override val endIndex: Int) : Token() @@ -347,7 +368,8 @@ class MathTokenizer private constructor() { * iterator will be at the token that comes after the last confirmed character in the string. */ private fun PeekableIterator.expectNextCharsForFunctionName( - chars: String, startIndex: Int + chars: String, + startIndex: Int ): Token? { for (c in chars) { expectNextValue { c } diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index e24c93ef740..83be344b1c0 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -12,26 +12,23 @@ import com.google.common.truth.Truth.assertWithMessage import com.google.common.truth.extensions.proto.LiteProtoSubject import org.junit.Test import org.junit.runner.RunWith +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.PRODUCT +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.MathFunctionCall import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT import org.oppia.android.app.model.MathUnaryOperation -import org.oppia.android.app.model.Real -import org.robolectric.annotation.LooperMode -import kotlin.math.sqrt -import org.oppia.android.app.model.ComparableOperationList -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.PRODUCT -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase -import org.oppia.android.app.model.MathEquation -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.OppiaLanguage import org.oppia.android.app.model.OppiaLanguage.ARABIC import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE @@ -42,6 +39,9 @@ import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED import org.oppia.android.app.model.Polynomial +import org.oppia.android.app.model.Real +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError @@ -64,8 +64,8 @@ import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError -import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode -import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode +import kotlin.math.sqrt /** Tests for [MathExpressionParser]. */ @RunWith(AndroidJUnit4::class) @@ -4061,7 +4061,9 @@ class MathExpressionParserTest { assertThat(equation1).hasRightHandSideThat().evaluatesToIntegerThat().isEqualTo(1) val equation2 = - parseAlgebraicEquationWithAllErrors("y = mx + b", allowedVariables = listOf("x", "y", "b", "m")) + parseAlgebraicEquationWithAllErrors( + "y = mx + b", allowedVariables = listOf("x", "y", "b", "m") + ) assertThat(equation2).hasLeftHandSideThat().hasStructureThatMatches { variable { withNameThat().isEqualTo("y") @@ -4170,7 +4172,9 @@ class MathExpressionParserTest { expectFailureWhenParsingAlgebraicEquation("y 2 = (x+1)(x-1)") val equation5 = - parseAlgebraicEquationWithAllErrors("a*x^2 + b*x + c = 0", allowedVariables = listOf("x", "a", "b", "c")) + parseAlgebraicEquationWithAllErrors( + "a*x^2 + b*x + c = 0", allowedVariables = listOf("x", "a", "b", "c") + ) assertThat(equation5).hasLeftHandSideThat().hasStructureThatMatches { addition { leftOperand { @@ -7379,7 +7383,8 @@ class MathExpressionParserTest { } private fun convertToHumanReadableString( - language: OppiaLanguage, divAsFraction: Boolean + language: OppiaLanguage, + divAsFraction: Boolean ): String { val readableString = maybeConvertToHumanReadableString(language, divAsFraction) assertWithMessage("Expected to convert to: $language").that(readableString).isNotNull() @@ -7641,15 +7646,24 @@ class MathExpressionParserTest { } private fun parseNumericExpressionWithoutOptionalErrors(expression: String): MathExpression { - return (parseNumericExpressionInternal(expression, ErrorCheckingMode.REQUIRED_ONLY) as MathParsingResult.Success).result + return ( + parseNumericExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY + ) as MathParsingResult.Success + ).result } private fun parseNumericExpressionWithAllErrors(expression: String): MathExpression { - return (parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) as MathParsingResult.Success).result + return ( + parseNumericExpressionInternal( + expression, ErrorCheckingMode.ALL_ERRORS + ) as MathParsingResult.Success + ).result } private fun parseNumericExpressionInternal( - expression: String, errorCheckingMode: ErrorCheckingMode + expression: String, + errorCheckingMode: ErrorCheckingMode ): MathParsingResult { return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) } @@ -7668,14 +7682,22 @@ class MathExpressionParserTest { expression: String, allowedVariables: List = listOf("x", "y", "z") ): MathExpression { - return (parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables) as MathParsingResult.Success).result + return ( + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables + ) as MathParsingResult.Success + ).result } private fun parseAlgebraicExpressionWithAllErrors( expression: String, allowedVariables: List = listOf("x", "y", "z") ): MathExpression { - return (parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) as MathParsingResult.Success).result + return ( + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables + ) as MathParsingResult.Success + ).result } private fun parseAlgebraicExpressionInternal( @@ -7698,7 +7720,11 @@ class MathExpressionParserTest { expression: String, allowedVariables: List = listOf("x", "y", "z") ): MathEquation { - return (parseAlgebraicEquationInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) as MathParsingResult.Success).result + return ( + parseAlgebraicEquationInternal( + expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables + ) as MathParsingResult.Success + ).result } private fun parseAlgebraicEquationInternal( From 9981829b48151a658ef5e300ae2afff9bed7a4a7 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 13 Dec 2021 19:14:40 -0800 Subject: [PATCH 091/289] Post-merge fixes. --- domain/src/main/java/org/oppia/android/domain/audio/BUILD.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/src/main/java/org/oppia/android/domain/audio/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/audio/BUILD.bazel index 440876b4862..c7141fc8c91 100644 --- a/domain/src/main/java/org/oppia/android/domain/audio/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/audio/BUILD.bazel @@ -33,7 +33,7 @@ kt_android_library( deps = [ "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//domain/src/main/java/org/oppia/android/domain/oppialogger:oppia_logger", - "//model:topic_java_proto_lite", + "//model/src/main/proto:topic_java_proto_lite", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/data:data_provider", ], From 17472e7eb0f5f10c937dfb7973bd6f8365e16ebe Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 15:35:29 -0800 Subject: [PATCH 092/289] Copy proto-based changes from #2173. --- app/BUILD.bazel | 12 +- .../oppia/android/app/activity/BUILD.bazel | 2 +- config/config_proto_assets.bzl | 4 +- .../java/org/oppia/android/config/BUILD.bazel | 4 +- data/BUILD.bazel | 2 +- .../android/data/backends/gae/BUILD.bazel | 2 +- .../android/data/persistence/BUILD.bazel | 2 +- domain/BUILD.bazel | 12 +- domain/domain_assets.bzl | 24 +- .../oppia/android/domain/audio/BUILD.bazel | 2 +- .../oppia/android/domain/locale/BUILD.bazel | 6 +- .../android/domain/onboarding/BUILD.bazel | 2 +- .../android/domain/oppialogger/BUILD.bazel | 2 +- .../domain/oppialogger/analytics/BUILD.bazel | 2 +- .../domain/oppialogger/exceptions/BUILD.bazel | 2 +- .../oppia/android/domain/state/BUILD.bazel | 8 +- .../android/domain/translation/BUILD.bazel | 12 +- .../org/oppia/android/domain/util/BUILD.bazel | 4 +- .../oppia/android/domain/locale/BUILD.bazel | 6 +- .../android/domain/translation/BUILD.bazel | 2 +- model/BUILD.bazel | 269 +----------------- model/oppia_proto_library.bzl | 19 ++ model/src/main/proto/BUILD.bazel | 257 +++++++++++++++++ .../proto/format_import_proto_library.bzl | 44 --- model/text_proto_assets.bzl | 4 +- scripts/script_assets.bzl | 14 +- .../oppia/android/scripts/common/BUILD.bazel | 2 +- .../oppia/android/testing/junit/BUILD.bazel | 2 +- .../oppia/android/testing/data/BUILD.bazel | 2 +- utility/BUILD.bazel | 6 +- .../org/oppia/android/util/locale/BUILD.bazel | 4 +- .../oppia/android/util/logging/BUILD.bazel | 4 +- .../android/util/logging/firebase/BUILD.bazel | 4 +- .../android/util/caching/testing/BUILD.bazel | 2 +- .../org/oppia/android/util/locale/BUILD.bazel | 2 +- 35 files changed, 357 insertions(+), 390 deletions(-) create mode 100644 model/oppia_proto_library.bzl create mode 100644 model/src/main/proto/BUILD.bazel delete mode 100644 model/src/main/proto/format_import_proto_library.bzl diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 0ae4da3c1ce..3e09ece2557 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -546,8 +546,8 @@ android_library( ":views", "//app/src/main/java/org/oppia/android/app/translation:app_language_activity_injector_provider", "//app/src/main/java/org/oppia/android/app/translation:app_language_resource_handler", - "//model:interaction_object_java_proto_lite", - "//model:thumbnail_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:thumbnail_java_proto_lite", "//third_party:androidx_annotation_annotation", "//third_party:androidx_constraintlayout_constraintlayout", "//third_party:androidx_core_core", @@ -579,8 +579,8 @@ kt_android_library( deps = [ ":dagger", "//domain/src/main/java/org/oppia/android/domain/audio:cellular_audio_dialog_controller", - "//model:question_java_proto_lite", - "//model:topic_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", + "//model/src/main/proto:topic_java_proto_lite", "//third_party:androidx_recyclerview_recyclerview", ], ) @@ -682,7 +682,7 @@ android_library( ":view_models", "//app/src/main/java/org/oppia/android/app/translation:app_language_activity_injector_provider", "//app/src/main/java/org/oppia/android/app/translation:app_language_resource_handler", - "//model:thumbnail_java_proto_lite", + "//model/src/main/proto:thumbnail_java_proto_lite", "//third_party:androidx_annotation_annotation", "//third_party:androidx_constraintlayout_constraintlayout", "//third_party:androidx_lifecycle_lifecycle-livedata-core", @@ -735,7 +735,7 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/oppialogger:startup_listener", "//domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions:logger_module", "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:worker_module", - "//model:arguments_java_proto_lite", + "//model/src/main/proto:arguments_java_proto_lite", "//app/src/main/java/org/oppia/android/app/testing/activity:test_activity", "//third_party:androidx_databinding_databinding-adapters", "//third_party:androidx_databinding_databinding-common", diff --git a/app/src/main/java/org/oppia/android/app/activity/BUILD.bazel b/app/src/main/java/org/oppia/android/app/activity/BUILD.bazel index 8b959707002..3c09724c453 100644 --- a/app/src/main/java/org/oppia/android/app/activity/BUILD.bazel +++ b/app/src/main/java/org/oppia/android/app/activity/BUILD.bazel @@ -75,6 +75,6 @@ kt_android_library( "//app:app_visibility", ], deps = [ - "//model:profile_java_proto_lite", + "//model/src/main/proto:profile_java_proto_lite", ], ) diff --git a/config/config_proto_assets.bzl b/config/config_proto_assets.bzl index f3073c33163..c45099babd2 100644 --- a/config/config_proto_assets.bzl +++ b/config/config_proto_assets.bzl @@ -23,8 +23,8 @@ def generate_supported_languages_configuration_from_text_proto( names = [supported_language_text_proto_file_name], proto_dep_name = "languages", proto_type_name = "SupportedLanguages", - name_prefix = name, + name_prefix = "supported_languages", asset_dir = "languages", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) diff --git a/config/src/java/org/oppia/android/config/BUILD.bazel b/config/src/java/org/oppia/android/config/BUILD.bazel index 36079416378..8cc9ae4591c 100644 --- a/config/src/java/org/oppia/android/config/BUILD.bazel +++ b/config/src/java/org/oppia/android/config/BUILD.bazel @@ -10,7 +10,7 @@ _SUPPORTED_LANGUAGES_CONFIG_ASSETS = generate_proto_binary_assets( asset_dir = "languages", name_prefix = "supported_languages_config_assets", names = ["supported_languages"], - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_dep_name = "languages", proto_package = "model", proto_type_name = "SupportedLanguages", @@ -21,7 +21,7 @@ _SUPPORTED_REGIONS_CONFIG_ASSETS = generate_proto_binary_assets( asset_dir = "languages", name_prefix = "supported_regions_config_assets", names = ["supported_regions"], - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_dep_name = "languages", proto_package = "model", proto_type_name = "SupportedRegions", diff --git a/data/BUILD.bazel b/data/BUILD.bazel index 3dc0dde487c..18fe1fd9127 100644 --- a/data/BUILD.bazel +++ b/data/BUILD.bazel @@ -14,7 +14,7 @@ TEST_DEPS = [ "//data/src/main/java/org/oppia/android/data/backends/gae:prod_module", "//data/src/main/java/org/oppia/android/data/backends/gae/model", "//data/src/main/java/org/oppia/android/data/persistence:cache_store", - "//model:test_models", + "//model/src/main/proto:test_models", "//testing", "//testing/src/main/java/org/oppia/android/testing/network", "//testing/src/main/java/org/oppia/android/testing/network:test_module", diff --git a/data/src/main/java/org/oppia/android/data/backends/gae/BUILD.bazel b/data/src/main/java/org/oppia/android/data/backends/gae/BUILD.bazel index 991bb9cb694..c5859673b0f 100644 --- a/data/src/main/java/org/oppia/android/data/backends/gae/BUILD.bazel +++ b/data/src/main/java/org/oppia/android/data/backends/gae/BUILD.bazel @@ -17,7 +17,7 @@ kt_android_library( deps = [ ":constants", ":network_config_annotations", - "//model:arguments_java_proto_lite", + "//model/src/main/proto:arguments_java_proto_lite", "//third_party:com_squareup_okhttp3_okhttp", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/extensions:context_extensions", diff --git a/data/src/main/java/org/oppia/android/data/persistence/BUILD.bazel b/data/src/main/java/org/oppia/android/data/persistence/BUILD.bazel index 0ead7ddfd08..db67d9fb798 100644 --- a/data/src/main/java/org/oppia/android/data/persistence/BUILD.bazel +++ b/data/src/main/java/org/oppia/android/data/persistence/BUILD.bazel @@ -12,7 +12,7 @@ kt_android_library( visibility = ["//:oppia_api_visibility"], deps = [ ":dagger", - "//model:profile_java_proto_lite", + "//model/src/main/proto:profile_java_proto_lite", "//utility", "//utility/src/main/java/org/oppia/android/util/data:async_data_subscription_manager", "//utility/src/main/java/org/oppia/android/util/data:async_result", diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index 0edd663c6e5..503c519682e 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -112,11 +112,11 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/util:asset", "//domain/src/main/java/org/oppia/android/domain/util:extensions", "//domain/src/main/java/org/oppia/android/domain/util:retriever", - "//model:exploration_checkpoint_java_proto_lite", - "//model:onboarding_java_proto_lite", - "//model:platform_parameter_java_proto_lite", - "//model:question_java_proto_lite", - "//model:topic_java_proto_lite", + "//model/src/main/proto:exploration_checkpoint_java_proto_lite", + "//model/src/main/proto:onboarding_java_proto_lite", + "//model/src/main/proto:platform_parameter_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", + "//model/src/main/proto:topic_java_proto_lite", "//third_party:androidx_work_work-runtime-ktx", "//utility/src/main/java/org/oppia/android/util/caching:topic_list_to_cache", "//utility/src/main/java/org/oppia/android/util/data:data_providers", @@ -149,7 +149,7 @@ kt_android_library( "src/test/java/org/oppia/android/domain/classify/InteractionObjectTestBuilder.kt", ], deps = [ - "//model:question_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", ], ) diff --git a/domain/domain_assets.bzl b/domain/domain_assets.bzl index dff28e34f9e..89a3ae008a3 100644 --- a/domain/domain_assets.bzl +++ b/domain/domain_assets.bzl @@ -32,53 +32,53 @@ def generate_assets_list_from_text_protos( names = topic_list_file_names, proto_dep_name = "topic", proto_type_name = "TopicIdList", - name_prefix = name, + name_prefix = "topic_id_list", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = topic_file_names, proto_dep_name = "topic", proto_type_name = "TopicRecord", - name_prefix = name, + name_prefix = "topic_record", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = subtopic_file_names, proto_dep_name = "topic", proto_type_name = "SubtopicRecord", - name_prefix = name, + name_prefix = "subtopic_record", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = story_file_names, proto_dep_name = "topic", proto_type_name = "StoryRecord", - name_prefix = name, + name_prefix = "story_record", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = skills_file_names, proto_dep_name = "topic", proto_type_name = "ConceptCardList", - name_prefix = name, + name_prefix = "concept_card_list", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = exploration_file_names, proto_dep_name = "exploration", proto_type_name = "Exploration", - name_prefix = name, + name_prefix = "exploration", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) diff --git a/domain/src/main/java/org/oppia/android/domain/audio/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/audio/BUILD.bazel index 440876b4862..c7141fc8c91 100644 --- a/domain/src/main/java/org/oppia/android/domain/audio/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/audio/BUILD.bazel @@ -33,7 +33,7 @@ kt_android_library( deps = [ "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//domain/src/main/java/org/oppia/android/domain/oppialogger:oppia_logger", - "//model:topic_java_proto_lite", + "//model/src/main/proto:topic_java_proto_lite", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/data:data_provider", ], diff --git a/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel index afc2f8b9fca..a62092b664e 100644 --- a/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel @@ -17,7 +17,7 @@ kt_android_library( ":display_locale_impl", ":language_config_retriever", "//domain/src/main/java/org/oppia/android/domain/oppialogger:oppia_logger", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/data:async_result", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale", @@ -64,7 +64,7 @@ kt_android_library( "//domain:domain_testing_visibility", ], deps = [ - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale", ], ) @@ -80,7 +80,7 @@ kt_android_library( deps = [ ":dagger", "//config/src/java/org/oppia/android/config:languages_config", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/caching:annotations", "//utility/src/main/java/org/oppia/android/util/caching:asset_repository", ], diff --git a/domain/src/main/java/org/oppia/android/domain/onboarding/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/onboarding/BUILD.bazel index eb14efb6c0a..f3ba2025c05 100644 --- a/domain/src/main/java/org/oppia/android/domain/onboarding/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/onboarding/BUILD.bazel @@ -15,7 +15,7 @@ kt_android_library( ":exploration_meta_data_retriever", "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//domain/src/main/java/org/oppia/android/domain/oppialogger:oppia_logger", - "//model:onboarding_java_proto_lite", + "//model/src/main/proto:onboarding_java_proto_lite", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/data:data_providers", diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel index e01fb432ed4..38af73c4546 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel @@ -13,7 +13,7 @@ kt_android_library( visibility = ["//domain:__subpackages__"], deps = [ "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:controller", - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/logging:console_logger", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel index 320025217d9..0166ef20c36 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel @@ -13,7 +13,7 @@ kt_android_library( deps = [ "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//domain/src/main/java/org/oppia/android/domain/oppialogger:storage_module", - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/logging:console_logger", "//utility/src/main/java/org/oppia/android/util/logging:event_logger", diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/BUILD.bazel index 0cd3f1b4a2b..af13183d30c 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/BUILD.bazel @@ -14,7 +14,7 @@ kt_android_library( deps = [ "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//domain/src/main/java/org/oppia/android/domain/oppialogger:storage_module", - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/logging:console_logger", "//utility/src/main/java/org/oppia/android/util/logging:exception_logger", diff --git a/domain/src/main/java/org/oppia/android/domain/state/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/state/BUILD.bazel index 5568e6221f2..aa32d0a3110 100644 --- a/domain/src/main/java/org/oppia/android/domain/state/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/state/BUILD.bazel @@ -11,7 +11,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:exploration_checkpoint_java_proto_lite", + "//model/src/main/proto:exploration_checkpoint_java_proto_lite", ], ) @@ -22,7 +22,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:exploration_checkpoint_java_proto_lite", + "//model/src/main/proto:exploration_checkpoint_java_proto_lite", ], ) @@ -33,7 +33,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:exploration_java_proto_lite", - "//model:question_java_proto_lite", + "//model/src/main/proto:exploration_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel index 4096a7f922f..700a8b0ad15 100644 --- a/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel @@ -13,12 +13,12 @@ kt_android_library( visibility = ["//:oppia_api_visibility"], deps = [ "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller", - "//model:interaction_object_java_proto_lite", - "//model:languages_java_proto_lite", - "//model:profile_java_proto_lite", - "//model:subtitled_html_java_proto_lite", - "//model:subtitled_unicode_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", + "//model/src/main/proto:profile_java_proto_lite", + "//model/src/main/proto:subtitled_html_java_proto_lite", + "//model/src/main/proto:subtitled_unicode_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/data:async_result", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/data:data_providers", diff --git a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel index 923a0a54e53..26dc7fb7027 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel @@ -31,7 +31,7 @@ kt_android_library( ], visibility = ["//domain:__subpackages__"], deps = [ - "//model:question_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", "//third_party:androidx_work_work-runtime-ktx", ], ) @@ -44,7 +44,7 @@ kt_android_library( visibility = ["//domain:__subpackages__"], deps = [ ":extensions", - "//model:question_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel index 6d992879342..b1a8a187562 100644 --- a/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel +++ b/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel @@ -34,7 +34,7 @@ oppia_android_test( deps = [ ":dagger", "//domain/src/main/java/org/oppia/android/domain/locale:content_locale_impl", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_extensions_truth-liteproto-extension", "//third_party:com_google_truth_truth", @@ -54,7 +54,7 @@ oppia_android_test( ":dagger", "//domain:test_resources", "//domain/src/main/java/org/oppia/android/domain/locale:display_locale_impl", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/time:test_module", "//third_party:androidx_test_ext_junit", @@ -121,7 +121,7 @@ oppia_android_test( deps = [ ":dagger", "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", diff --git a/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel index 0877354b02c..d4733f05076 100644 --- a/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel +++ b/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel @@ -15,7 +15,7 @@ oppia_android_test( ":dagger", "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller", "//domain/src/main/java/org/oppia/android/domain/translation:translation_controller", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", diff --git a/model/BUILD.bazel b/model/BUILD.bazel index d64d7e2a340..1755f53361e 100644 --- a/model/BUILD.bazel +++ b/model/BUILD.bazel @@ -1,270 +1,3 @@ -# TODO(#1532): Rename file to 'BUILD' post-Gradle. """ -This library contains all protos used in the app and is a dependency for all other modules. -In Bazel, proto files are built using the proto_library() and java_lite_proto_library() rules. -The proto_library() rule creates a proto file library to be used in multiple languages. -The java_lite_proto_library() rule takes in a proto_library target and generates java code. +TODO: add docs """ - -load("@rules_java//java:defs.bzl", "java_lite_proto_library") -load("@rules_proto//proto:defs.bzl", "proto_library") -load("//model:src/main/proto/format_import_proto_library.bzl", "format_import_proto_library") - -# NOTE TO DEVELOPERS: When adding new protos, each proto will need to have both a proto_library -# and java_lite_proto_library. See the examples below for context. Further, once the proto lite -# library is added, it should be included in the exports list in the model library at the -# bottom of this file so that other parts of the app get access to it. If protos import other -# protos, they need to use format_import_proto_library (again, see examples below for how to do -# this). -# -# For example, if adding a new proto file called 'important_structure.proto', add these: -# proto_library( -# name = "important_structure_proto", -# srcs = ["src/main/proto/important_structure.proto"], -# ) -# -# java_lite_proto_library( -# name = "important_structure_java_proto_lite", -# deps = [":important_structure_proto"], -# ) -# -# And change the 'model' library at the bottom of the file, e.g.: -# android_library( -# name = "model", -# exports = [ -# ... -# ":important_structure_java_proto_lite", -# ... -# ], -# ... -# ) - -proto_library( - name = "arguments_proto", - srcs = ["src/main/proto/arguments.proto"], -) - -java_lite_proto_library( - name = "arguments_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":arguments_proto"], -) - -proto_library( - name = "event_logger_proto", - srcs = ["src/main/proto/oppia_logger.proto"], -) - -java_lite_proto_library( - name = "event_logger_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":event_logger_proto"], -) - -format_import_proto_library( - name = "exploration_checkpoint", - src = "src/main/proto/exploration_checkpoint.proto", - deps = [":exploration_proto"], -) - -java_lite_proto_library( - name = "exploration_checkpoint_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":exploration_checkpoint_proto"], -) - -proto_library( - name = "interaction_object_proto", - srcs = ["src/main/proto/interaction_object.proto"], -) - -java_lite_proto_library( - name = "interaction_object_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":interaction_object_proto"], -) - -proto_library( - name = "languages_proto", - srcs = ["src/main/proto/languages.proto"], - visibility = ["//visibility:public"], -) - -java_lite_proto_library( - name = "languages_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":languages_proto"], -) - -proto_library( - name = "onboarding_proto", - srcs = ["src/main/proto/onboarding.proto"], -) - -java_lite_proto_library( - name = "onboarding_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":onboarding_proto"], -) - -proto_library( - name = "profile_proto", - srcs = ["src/main/proto/profile.proto"], -) - -java_lite_proto_library( - name = "profile_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":profile_proto"], -) - -proto_library( - name = "subtitled_html_proto", - srcs = ["src/main/proto/subtitled_html.proto"], -) - -java_lite_proto_library( - name = "subtitled_html_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":subtitled_html_proto"], -) - -proto_library( - name = "subtitled_unicode_proto", - srcs = ["src/main/proto/subtitled_unicode.proto"], -) - -java_lite_proto_library( - name = "subtitled_unicode_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":subtitled_unicode_proto"], -) - -proto_library( - name = "test_proto", - srcs = ["src/main/proto/test.proto"], -) - -java_lite_proto_library( - name = "test_java_proto_lite", - deps = [":test_proto"], -) - -proto_library( - name = "thumbnail_proto", - srcs = ["src/main/proto/thumbnail.proto"], -) - -java_lite_proto_library( - name = "thumbnail_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":thumbnail_proto"], -) - -proto_library( - name = "translation_proto", - srcs = ["src/main/proto/translation.proto"], -) - -java_lite_proto_library( - name = "translation_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":translation_proto"], -) - -proto_library( - name = "voiceover_proto", - srcs = ["src/main/proto/voiceover.proto"], -) - -java_lite_proto_library( - name = "voiceover_java_proto_lite", - deps = [":voiceover_proto"], -) - -format_import_proto_library( - name = "feedback_reporting", - src = "src/main/proto/feedback_reporting.proto", - deps = [ - ":profile_proto", - ], -) - -java_lite_proto_library( - name = "feedback_reporting_java_proto_lite", - deps = [":feedback_reporting_proto"], -) - -format_import_proto_library( - name = "question", - src = "src/main/proto/question.proto", - deps = [ - ":exploration_proto", - ":subtitled_html_proto", - ":subtitled_unicode_proto", - ], -) - -java_lite_proto_library( - name = "question_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":question_proto"], -) - -format_import_proto_library( - name = "topic", - src = "src/main/proto/topic.proto", - visibility = ["//visibility:public"], - deps = [ - ":subtitled_html_proto", - ":subtitled_unicode_proto", - ":thumbnail_proto", - ":translation_proto", - ":voiceover_proto", - ], -) - -java_lite_proto_library( - name = "topic_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":topic_proto"], -) - -format_import_proto_library( - name = "exploration", - src = "src/main/proto/exploration.proto", - visibility = ["//visibility:public"], - deps = [ - ":interaction_object_proto", - ":subtitled_html_proto", - ":subtitled_unicode_proto", - ":translation_proto", - ":voiceover_proto", - ], -) - -java_lite_proto_library( - name = "exploration_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":exploration_proto"], -) - -format_import_proto_library( - name = "platform_parameter", - src = "src/main/proto/platform_parameter.proto", -) - -java_lite_proto_library( - name = "platform_parameter_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":platform_parameter_proto"], -) - -android_library( - name = "test_models", - testonly = True, - visibility = ["//visibility:public"], - exports = [ - ":test_java_proto_lite", - ], -) diff --git a/model/oppia_proto_library.bzl b/model/oppia_proto_library.bzl new file mode 100644 index 00000000000..8f6ac753135 --- /dev/null +++ b/model/oppia_proto_library.bzl @@ -0,0 +1,19 @@ +""" +TODO: add docs +""" + +load("@rules_proto//proto:defs.bzl", "proto_library") + +# TODO: add regex check +# TODO: add TODO to remove +# TODO: maybe close format proto issue with this PR? + +def oppia_proto_library(name, strip_import_prefix = "", **kwargs): + """ + TODO: add docs + """ + proto_library( + name = name, + strip_import_prefix = strip_import_prefix, + **kwargs + ) diff --git a/model/src/main/proto/BUILD.bazel b/model/src/main/proto/BUILD.bazel new file mode 100644 index 00000000000..90da6e48b77 --- /dev/null +++ b/model/src/main/proto/BUILD.bazel @@ -0,0 +1,257 @@ +# TODO(#1532): Rename file to 'BUILD' post-Gradle. +""" +This library contains all protos used in the app and is a dependency for all other modules. +In Bazel, proto files are built using the oppia_proto_library() and java_lite_proto_library() rules. +The oppia_proto_library() rule creates a proto file library to be used in multiple languages. +The java_lite_proto_library() rule takes in a proto_library target and generates java code. +""" + +load("@rules_java//java:defs.bzl", "java_lite_proto_library") +load("//model:oppia_proto_library.bzl", "oppia_proto_library") + +# NOTE TO DEVELOPERS: When adding new protos, each proto will need to have both a proto_library +# and java_lite_proto_library. +# +# For example, if adding a new proto file called 'important_structure.proto', add these: +# oppia_proto_library( +# name = "important_structure_proto", +# srcs = ["src/main/proto/important_structure.proto"], +# ) +# +# java_lite_proto_library( +# name = "important_structure_java_proto_lite", +# deps = [":important_structure_proto"], +# ) + +oppia_proto_library( + name = "arguments_proto", + srcs = ["arguments.proto"], +) + +java_lite_proto_library( + name = "arguments_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":arguments_proto"], +) + +oppia_proto_library( + name = "event_logger_proto", + srcs = ["oppia_logger.proto"], +) + +java_lite_proto_library( + name = "event_logger_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":event_logger_proto"], +) + +oppia_proto_library( + name = "exploration_checkpoint_proto", + srcs = ["exploration_checkpoint.proto"], + deps = [":exploration_proto"], +) + +java_lite_proto_library( + name = "exploration_checkpoint_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":exploration_checkpoint_proto"], +) + +oppia_proto_library( + name = "interaction_object_proto", + srcs = ["interaction_object.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "interaction_object_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":interaction_object_proto"], +) + +oppia_proto_library( + name = "languages_proto", + srcs = ["languages.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "languages_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":languages_proto"], +) + +oppia_proto_library( + name = "onboarding_proto", + srcs = ["onboarding.proto"], +) + +java_lite_proto_library( + name = "onboarding_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":onboarding_proto"], +) + +oppia_proto_library( + name = "profile_proto", + srcs = ["profile.proto"], +) + +java_lite_proto_library( + name = "profile_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":profile_proto"], +) + +oppia_proto_library( + name = "subtitled_html_proto", + srcs = ["subtitled_html.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "subtitled_html_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":subtitled_html_proto"], +) + +oppia_proto_library( + name = "subtitled_unicode_proto", + srcs = ["subtitled_unicode.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "subtitled_unicode_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":subtitled_unicode_proto"], +) + +oppia_proto_library( + name = "test_proto", + srcs = ["test.proto"], +) + +java_lite_proto_library( + name = "test_java_proto_lite", + deps = [":test_proto"], +) + +oppia_proto_library( + name = "thumbnail_proto", + srcs = ["thumbnail.proto"], +) + +java_lite_proto_library( + name = "thumbnail_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":thumbnail_proto"], +) + +oppia_proto_library( + name = "translation_proto", + srcs = ["translation.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "translation_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":translation_proto"], +) + +oppia_proto_library( + name = "voiceover_proto", + srcs = ["voiceover.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "voiceover_java_proto_lite", + deps = [":voiceover_proto"], +) + +oppia_proto_library( + name = "feedback_reporting_proto", + srcs = ["feedback_reporting.proto"], + deps = [":profile_proto"], +) + +java_lite_proto_library( + name = "feedback_reporting_java_proto_lite", + deps = [":feedback_reporting_proto"], +) + +oppia_proto_library( + name = "question_proto", + srcs = ["question.proto"], + deps = [ + ":exploration_proto", + ":subtitled_html_proto", + ":subtitled_unicode_proto", + ], +) + +java_lite_proto_library( + name = "question_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":question_proto"], +) + +oppia_proto_library( + name = "topic_proto", + srcs = ["topic.proto"], + visibility = ["//:oppia_api_visibility"], + deps = [ + ":subtitled_html_proto", + ":subtitled_unicode_proto", + ":thumbnail_proto", + ":translation_proto", + ":voiceover_proto", + ], +) + +java_lite_proto_library( + name = "topic_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":topic_proto"], +) + +oppia_proto_library( + name = "exploration_proto", + srcs = ["exploration.proto"], + visibility = ["//:oppia_api_visibility"], + deps = [ + ":interaction_object_proto", + ":subtitled_html_proto", + ":subtitled_unicode_proto", + ":translation_proto", + ":voiceover_proto", + ], +) + +java_lite_proto_library( + name = "exploration_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":exploration_proto"], +) + +oppia_proto_library( + name = "platform_parameter_proto", + srcs = ["platform_parameter.proto"], +) + +java_lite_proto_library( + name = "platform_parameter_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":platform_parameter_proto"], +) + +android_library( + name = "test_models", + testonly = True, + visibility = ["//:oppia_api_visibility"], + exports = [ + ":test_java_proto_lite", + ], +) diff --git a/model/src/main/proto/format_import_proto_library.bzl b/model/src/main/proto/format_import_proto_library.bzl deleted file mode 100644 index 77ab61f0fdf..00000000000 --- a/model/src/main/proto/format_import_proto_library.bzl +++ /dev/null @@ -1,44 +0,0 @@ -""" -Container for macros to fix proto files. -""" - -load("@rules_proto//proto:defs.bzl", "proto_library") - -def format_import_proto_library(name, src, deps = [], **kwargs): - """ - Creates a new proto library with corrected imports. - - This macro exists as a way to build proto files that contain import statements in both Gradle - and Bazel. This macro formats the src file's import statements to contain a full path to the - file in order for Bazel to properly locate file. - - Args: - name: str. The name of the .proto file without the '.proto' suffix. This will be the root for - the name of the proto library created. Ex: If name = 'topic', then the src file is - 'topic.proto' and the proto library created will be named 'topic_proto'. - src: str. The name of the .proto file to be built into a proto_library. - deps: list of str. The list of dependencies needed to build the src file. This list will - contain all of the proto_library targets for the files imported into src. - **kwargs: additional parameters passed in. - """ - - # TODO(#1543): Ensure this function works on Windows systems. - # TODO(#1617): Remove genrules post-gradle - native.genrule( - name = name, - srcs = [src], - outs = ["processed_" + src], - cmd = """ - cat $< | - sed 's/import "/import "model\\/src\\/main\\/proto\\//g' | - sed 's/"model\\/src\\/main\\/proto\\/exploration/"model\\/processed_src\\/main\\/proto\\/exploration/g' | - sed 's/"model\\/src\\/main\\/proto\\/topic/"model\\/processed_src\\/main\\/proto\\/topic/g' | - sed 's/"model\\/src\\/main\\/proto\\/question/"model\\/processed_src\\/main\\/proto\\/question/g' > $@ - """, - ) - proto_library( - name = name + "_proto", - srcs = ["processed_" + src], - deps = deps, - **kwargs - ) diff --git a/model/text_proto_assets.bzl b/model/text_proto_assets.bzl index 326dd00933a..ace61c6a81b 100644 --- a/model/text_proto_assets.bzl +++ b/model/text_proto_assets.bzl @@ -29,9 +29,11 @@ def _gen_binary_proto_from_text_impl(ctx): # proto to binary, and expected stdin/stdout configurations. Note that the actual proto files # are passed to the compiler since it requires them in order to transcode the text proto file. command_path = ctx.executable._protoc_tool.path + proto_directory_path_args = ["--proto_path=%s" % file.dirname for file in input_proto_files] + proto_file_names = [file.basename for file in input_proto_files] arguments = [command_path] + [ "--encode %s" % ctx.attr.proto_type_name, - ] + [file.path for file in input_proto_files] + [ + ] + proto_directory_path_args + proto_file_names + [ "< %s" % input_file, "> %s" % output_file.path, ] diff --git a/scripts/script_assets.bzl b/scripts/script_assets.bzl index b5c3fb389e4..363455fb553 100644 --- a/scripts/script_assets.bzl +++ b/scripts/script_assets.bzl @@ -24,7 +24,7 @@ def generate_regex_assets_list_from_text_protos( names = filepath_pattern_validation_file_names, proto_dep_name = "filename_pattern_validation_checks", proto_type_name = "FilenameChecks", - name_prefix = name, + name_prefix = "filename_checks", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -33,7 +33,7 @@ def generate_regex_assets_list_from_text_protos( names = file_content_validation_file_names, proto_dep_name = "file_content_validation_checks", proto_type_name = "FileContentChecks", - name_prefix = name, + name_prefix = "file_content_checks", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -57,7 +57,7 @@ def generate_test_file_assets_list_from_text_protos( names = test_file_exemptions_name, proto_dep_name = "script_exemptions", proto_type_name = "TestFileExemptions", - name_prefix = name, + name_prefix = "test_file_exemptions", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -82,7 +82,7 @@ def generate_maven_assets_list_from_text_protos( names = maven_dependency_filenames, proto_dep_name = "maven_dependencies", proto_type_name = "MavenDependencyList", - name_prefix = name, + name_prefix = "maven_dependency_list", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -107,7 +107,7 @@ def generate_accessibility_label_assets_list_from_text_protos( names = accessibility_label_exemptions_name, proto_dep_name = "script_exemptions", proto_type_name = "AccessibilityLabelExemptions", - name_prefix = name, + name_prefix = "accessibility_label_exemptions", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -131,7 +131,7 @@ def generate_kdoc_validity_assets_list_from_text_protos( names = kdoc_validity_exemptions_name, proto_dep_name = "script_exemptions", proto_type_name = "KdocValidityExemptions", - name_prefix = name, + name_prefix = "kdoc_validity_exemptions", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -155,7 +155,7 @@ def generate_todo_assets_list_from_text_protos( names = todo_exemptions_name, proto_dep_name = "script_exemptions", proto_type_name = "TodoOpenExemptions", - name_prefix = name, + name_prefix = "todo_open_exemptions", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", diff --git a/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel b/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel index b4559459055..e47fc741ab6 100644 --- a/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel +++ b/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel @@ -43,7 +43,7 @@ kt_jvm_test( name = "ProtoStringEncoderTest", srcs = ["ProtoStringEncoderTest.kt"], deps = [ - "//model:test_models", + "//model/src/main/proto:test_models", "//scripts/src/java/org/oppia/android/scripts/common:proto_string_encoder", "//testing:assertion_helpers", "//third_party:com_google_truth_extensions_truth-liteproto-extension", diff --git a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel index e2c54937b89..e8d62702d99 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel @@ -25,7 +25,7 @@ kt_android_library( ":define_app_language_locale_context", "//domain/src/main/java/org/oppia/android/domain/locale:locale_application_injector", "//domain/src/main/java/org/oppia/android/domain/locale:locale_application_injector_provider", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//third_party:androidx_test_core", "//third_party:junit_junit", ], diff --git a/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel b/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel index 4b84736203b..bfc5a1ab0ac 100644 --- a/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel +++ b/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel @@ -16,7 +16,7 @@ oppia_android_test( ":dagger", "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller", "//domain/src/main/java/org/oppia/android/domain/translation:translation_controller", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", diff --git a/utility/BUILD.bazel b/utility/BUILD.bazel index 98a35c2ae8f..99cc8627c08 100644 --- a/utility/BUILD.bazel +++ b/utility/BUILD.bazel @@ -54,8 +54,8 @@ kt_android_library( ":resources", "//app:crashlytics", "//app:crashlytics_deps", - "//model:event_logger_java_proto_lite", - "//model:platform_parameter_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", + "//model/src/main/proto:platform_parameter_java_proto_lite", "//third_party:androidx_appcompat_appcompat", "//third_party:androidx_room_room-runtime", "//third_party:androidx_work_work-runtime", @@ -91,7 +91,7 @@ TEST_DEPS = [ ":utility", "//app:crashlytics", "//app:crashlytics_deps", - "//model:test_models", + "//model/src/main/proto:test_models", "//testing", "//testing/src/main/java/org/oppia/android/testing/mockito", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", diff --git a/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel index 1522fe1a28a..0c57bde04d0 100644 --- a/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel @@ -36,7 +36,7 @@ kt_android_library( visibility = ["//:oppia_api_visibility"], deps = [ ":oppia_locale_context_extensions", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//third_party:androidx_annotation_annotation", ], ) @@ -63,7 +63,7 @@ kt_android_library( "OppiaLocaleContextExtensions.kt", ], deps = [ - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", ], ) diff --git a/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel index 3ea1f8bbe89..21b2fa9154c 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel @@ -39,7 +39,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", ], ) @@ -50,7 +50,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", ], ) diff --git a/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel index 3a69f05bcf2..7c5e92b2d2d 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel @@ -23,7 +23,7 @@ kt_android_library( "FirebaseLogUploader.kt", ], deps = [ - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//third_party:androidx_work_work-runtime", "//third_party:androidx_work_work-runtime-ktx", "//third_party:com_google_firebase_firebase-analytics", @@ -62,7 +62,7 @@ kt_android_library( "//app:__pkg__", ], deps = [ - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/logging:event_logger", ], diff --git a/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel index a4187f0dede..f9cf0e1a190 100644 --- a/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel @@ -31,7 +31,7 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ ":dagger", - "//model:test_models", + "//model/src/main/proto:test_models", "//testing", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_extensions_truth-liteproto-extension", diff --git a/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel index 43c6ea60c0c..075455e6297 100644 --- a/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel @@ -87,7 +87,7 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ ":dagger", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_extensions_truth-liteproto-extension", "//third_party:junit_junit", From fe73a2f8083dbd48ab207068a7931ff4733bd9f4 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 17:34:44 -0800 Subject: [PATCH 093/289] Introduce math.proto & refactor math extensions. Much of this is copied from #2173. --- ...atioExpressionInputInteractionViewModel.kt | 2 +- domain/BUILD.bazel | 1 + ...AndInSimplestFormRuleClassifierProvider.kt | 6 +++--- ...putIsEquivalentToRuleClassifierProvider.kt | 4 ++-- ...nputIsGreaterThanRuleClassifierProvider.kt | 2 +- ...onInputIsLessThanRuleClassifierProvider.kt | 2 +- ...ithUnitsIsEqualToRuleClassifierProvider.kt | 2 +- ...itsIsEquivalentToRuleClassifierProvider.kt | 4 ++-- ...umericInputEqualsRuleClassifierProvider.kt | 2 +- ...InputIsEquivalentRuleClassifierProvider.kt | 2 +- .../org/oppia/android/domain/util/BUILD.bazel | 4 +--- .../util/InteractionObjectExtensions.kt | 1 + model/src/main/proto/BUILD.bazel | 13 ++++++++++++ model/src/main/proto/interaction_object.proto | 17 ++------------- model/src/main/proto/math.proto | 21 +++++++++++++++++++ utility/BUILD.bazel | 1 + .../org/oppia/android/util/math/BUILD.bazel | 20 ++++++++++++++++++ .../android/util/math}/FloatExtensions.kt | 4 ++-- .../android/util/math}/FractionExtensions.kt | 2 +- .../android/util/math}/RatioExtensions.kt | 2 +- 20 files changed, 77 insertions(+), 35 deletions(-) create mode 100644 model/src/main/proto/math.proto create mode 100644 utility/src/main/java/org/oppia/android/util/math/BUILD.bazel rename {domain/src/main/java/org/oppia/android/domain/util => utility/src/main/java/org/oppia/android/util/math}/FloatExtensions.kt (86%) rename {domain/src/main/java/org/oppia/android/domain/util => utility/src/main/java/org/oppia/android/util/math}/FractionExtensions.kt (96%) rename {domain/src/main/java/org/oppia/android/domain/util => utility/src/main/java/org/oppia/android/util/math}/RatioExtensions.kt (94%) diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt index 064b7fc3f60..6f916b0f4d0 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt @@ -16,7 +16,7 @@ import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandle import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.utility.toAccessibleAnswerString import org.oppia.android.domain.translation.TranslationController -import org.oppia.android.domain.util.toAnswerString +import org.oppia.android.util.math.toAnswerString /** [StateItemViewModel] for the ratio expression input interaction. */ class RatioExpressionInputInteractionViewModel( diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index 503c519682e..cf4b8098c8e 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -124,6 +124,7 @@ kt_android_library( "//utility/src/main/java/org/oppia/android/util/extensions:context_extensions", "//utility/src/main/java/org/oppia/android/util/logging:event_logger", "//utility/src/main/java/org/oppia/android/util/logging:log_uploader", + "//utility/src/main/java/org/oppia/android/util/math:extensions", "//utility/src/main/java/org/oppia/android/util/networking:network_connection_util", "//utility/src/main/java/org/oppia/android/util/profile:directory_management_util", ], diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt index 76ed9cb0f72..598d7a71ad1 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt @@ -6,9 +6,9 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals -import org.oppia.android.domain.util.toFloat -import org.oppia.android.domain.util.toSimplestForm +import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.toFloat +import org.oppia.android.util.math.toSimplestForm import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt index d7f8c597461..0c47a5d0657 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt @@ -6,8 +6,8 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals -import org.oppia.android.domain.util.toFloat +import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.toFloat import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt index 740ab078eee..a44dedfcfdf 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt @@ -6,7 +6,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.toFloat +import org.oppia.android.util.math.toFloat import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt index 3fbf98e8fac..52d1396d9f1 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt @@ -6,7 +6,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.toFloat +import org.oppia.android.util.math.toFloat import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt index e8375af7173..9a225cc41ca 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt @@ -7,7 +7,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals +import org.oppia.android.util.math.approximatelyEquals import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt index daab9473b4e..99bad528bb4 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt @@ -6,8 +6,8 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals -import org.oppia.android.domain.util.toFloat +import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.toFloat import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt index f5c84525281..2c7a6dc5212 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt @@ -5,7 +5,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals +import org.oppia.android.util.math.approximatelyEquals import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt index 57aba25a07c..f9b9b2e9df8 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt @@ -6,7 +6,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.toSimplestForm +import org.oppia.android.util.math.toSimplestForm import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel index 26dc7fb7027..7c1dc1e6ce2 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel @@ -21,11 +21,8 @@ kt_android_library( kt_android_library( name = "extensions", srcs = [ - "FloatExtensions.kt", - "FractionExtensions.kt", "InteractionObjectExtensions.kt", "JsonExtensions.kt", - "RatioExtensions.kt", "StringExtensions.kt", "WorkDataExtensions.kt", ], @@ -33,6 +30,7 @@ kt_android_library( deps = [ "//model/src/main/proto:question_java_proto_lite", "//third_party:androidx_work_work-runtime-ktx", + "//utility/src/main/java/org/oppia/android/util/math:extensions", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt b/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt index a4d813c6ec8..2e37e34e0f7 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt +++ b/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt @@ -29,6 +29,7 @@ import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds import org.oppia.android.app.model.StringList import org.oppia.android.app.model.TranslatableHtmlContentId import org.oppia.android.app.model.TranslatableSetOfNormalizedString +import org.oppia.android.util.math.toAnswerString /** * Returns a parsable string representation of a user-submitted answer version of this diff --git a/model/src/main/proto/BUILD.bazel b/model/src/main/proto/BUILD.bazel index 90da6e48b77..f28661f7341 100644 --- a/model/src/main/proto/BUILD.bazel +++ b/model/src/main/proto/BUILD.bazel @@ -61,6 +61,7 @@ oppia_proto_library( name = "interaction_object_proto", srcs = ["interaction_object.proto"], visibility = ["//:oppia_api_visibility"], + deps = [":math_proto"], ) java_lite_proto_library( @@ -81,6 +82,18 @@ java_lite_proto_library( deps = [":languages_proto"], ) +oppia_proto_library( + name = "math_proto", + srcs = ["math.proto"], + strip_import_prefix = "", +) + +java_lite_proto_library( + name = "math_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":math_proto"], +) + oppia_proto_library( name = "onboarding_proto", srcs = ["onboarding.proto"], diff --git a/model/src/main/proto/interaction_object.proto b/model/src/main/proto/interaction_object.proto index c444f316431..bb6154f9255 100644 --- a/model/src/main/proto/interaction_object.proto +++ b/model/src/main/proto/interaction_object.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package model; +import "math.proto"; + option java_package = "org.oppia.android.app.model"; option java_multiple_files = true; @@ -35,13 +37,6 @@ message StringList { repeated string html = 1; } -// Structure containing a ratio object for eg - [1,2,3] for 1:2:3. -message RatioExpression { - // List of components in a ratio. It's expected that list should have more than - // 1 element. - repeated uint32 ratio_component = 1; -} - // Structure for a number with units object. message NumberWithUnits { oneof number_type { @@ -57,14 +52,6 @@ message NumberUnit { int32 exponent = 2; } -// Structure for a fraction object. -message Fraction { - bool is_negative = 1; - int32 whole_number = 2; - int32 numerator = 3; - int32 denominator = 4; -} - // Structure for a ListOfString object. message ListOfSetsOfHtmlStrings { repeated StringList set_of_html_strings = 1; diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto new file mode 100644 index 00000000000..0288db3148b --- /dev/null +++ b/model/src/main/proto/math.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package model; + +option java_package = "org.oppia.android.app.model"; +option java_multiple_files = true; + +// Structure for a fraction object. +message Fraction { + bool is_negative = 1; + int32 whole_number = 2; + int32 numerator = 3; + int32 denominator = 4; +} + +// Structure containing a ratio object for eg - [1,2,3] for 1:2:3. +message RatioExpression { + // List of components in a ratio. It's expected that list should have more than + // 1 element. + repeated uint32 ratio_component = 1; +} diff --git a/utility/BUILD.bazel b/utility/BUILD.bazel index 99cc8627c08..513122f87ef 100644 --- a/utility/BUILD.bazel +++ b/utility/BUILD.bazel @@ -19,6 +19,7 @@ MIGRATED_PROD_FILES = glob([ "src/main/java/org/oppia/android/util/extensions/*.kt", "src/main/java/org/oppia/android/util/gcsresource/*.kt", "src/main/java/org/oppia/android/util/logging/*.kt", + "src/main/java/org/oppia/android/util/math/**/*.kt", "src/main/java/org/oppia/android/util/networking/*.kt", "src/main/java/org/oppia/android/util/profile/*.kt", "src/main/java/org/oppia/android/util/statusbar/*.kt", diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel new file mode 100644 index 00000000000..4b84961d297 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -0,0 +1,20 @@ +""" +TODO: document +""" + +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "extensions", + srcs = [ + "FloatExtensions.kt", + "FractionExtensions.kt", + "RatioExtensions.kt", + ], + visibility = [ + "//:oppia_api_visibility", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + ], +) diff --git a/domain/src/main/java/org/oppia/android/domain/util/FloatExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt similarity index 86% rename from domain/src/main/java/org/oppia/android/domain/util/FloatExtensions.kt rename to utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt index 5ce8d4b0c10..62504046c78 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/FloatExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt @@ -1,9 +1,9 @@ -package org.oppia.android.domain.util +package org.oppia.android.util.math import kotlin.math.abs /** The error margin used for float equality by [Float.approximatelyEquals]. */ -public const val FLOAT_EQUALITY_INTERVAL = 1e-5 +const val FLOAT_EQUALITY_INTERVAL = 1e-5 /** Returns whether this float approximately equals another based on a consistent epsilon value. */ fun Float.approximatelyEquals(other: Float): Boolean { diff --git a/domain/src/main/java/org/oppia/android/domain/util/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt similarity index 96% rename from domain/src/main/java/org/oppia/android/domain/util/FractionExtensions.kt rename to utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index 878576da012..69b57d5be39 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -1,4 +1,4 @@ -package org.oppia.android.domain.util +package org.oppia.android.util.math import org.oppia.android.app.model.Fraction diff --git a/domain/src/main/java/org/oppia/android/domain/util/RatioExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt similarity index 94% rename from domain/src/main/java/org/oppia/android/domain/util/RatioExtensions.kt rename to utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt index 821fa274e31..123a24e2958 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/RatioExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt @@ -1,4 +1,4 @@ -package org.oppia.android.domain.util +package org.oppia.android.util.math import org.oppia.android.app.model.RatioExpression From d17e3dc68fb98dc2c0b666194145afa397a5a09a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 17:49:47 -0800 Subject: [PATCH 094/289] Migrate tests & remove unneeded prefix. --- model/src/main/proto/BUILD.bazel | 1 - .../org/oppia/android/util/math/BUILD.bazel | 22 +++++++++++++++++++ .../android/util/math}/RatioExtensionsTest.kt | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 utility/src/test/java/org/oppia/android/util/math/BUILD.bazel rename {domain/src/test/java/org/oppia/android/domain/util => utility/src/test/java/org/oppia/android/util/math}/RatioExtensionsTest.kt (97%) diff --git a/model/src/main/proto/BUILD.bazel b/model/src/main/proto/BUILD.bazel index f28661f7341..a88f7ec5ed0 100644 --- a/model/src/main/proto/BUILD.bazel +++ b/model/src/main/proto/BUILD.bazel @@ -85,7 +85,6 @@ java_lite_proto_library( oppia_proto_library( name = "math_proto", srcs = ["math.proto"], - strip_import_prefix = "", ) java_lite_proto_library( diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel new file mode 100644 index 00000000000..493d89d66a0 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -0,0 +1,22 @@ +""" +TODO: document +""" + +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "RatioExtensionsTest", + srcs = ["RatioExtensionsTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.RatioExtensionsTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + ], +) diff --git a/domain/src/test/java/org/oppia/android/domain/util/RatioExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RatioExtensionsTest.kt similarity index 97% rename from domain/src/test/java/org/oppia/android/domain/util/RatioExtensionsTest.kt rename to utility/src/test/java/org/oppia/android/util/math/RatioExtensionsTest.kt index dffb8e65b2f..ca380220b0b 100644 --- a/domain/src/test/java/org/oppia/android/domain/util/RatioExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RatioExtensionsTest.kt @@ -1,4 +1,4 @@ -package org.oppia.android.domain.util +package org.oppia.android.util.math import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat From 986851fa390c024f455bdcc22cdeb973dee80305 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 17:53:56 -0800 Subject: [PATCH 095/289] Post-merge fix. --- .../src/main/java/org/oppia/android/util/math/RatioExtensions.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt index 0097ce9e8bf..382706db3c6 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt @@ -1,7 +1,6 @@ package org.oppia.android.util.math import org.oppia.android.app.model.RatioExpression -import org.oppia.android.util.math.gcd // TODO: move to new package. From fb61e39bf1f598946e534a2d77a8c456d07f149a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 17:55:46 -0800 Subject: [PATCH 096/289] Add needed newline. --- .../src/main/java/org/oppia/android/util/math/RatioExtensions.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt index 123a24e2958..83d85e9098c 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt @@ -13,6 +13,7 @@ fun RatioExpression.toSimplestForm(): List { this.ratioComponentList.map { x -> x / gcdComponentResult } } } + /** * Returns this Ratio in string format. * E.g. [1, 2, 3] will yield to 1:2:3 From 7ca0e9adde4c9f11a3ae7f42d749494196d6f3a3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 17:58:28 -0800 Subject: [PATCH 097/289] Remove post-merge unnecessary comment. --- .../main/java/org/oppia/android/util/math/RatioExtensions.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt index 382706db3c6..83d85e9098c 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt @@ -2,8 +2,6 @@ package org.oppia.android.util.math import org.oppia.android.app.model.RatioExpression -// TODO: move to new package. - /** * Returns this Ratio in its most simplified form. */ From acab98bd74477a78a55605364df20b4ef029050f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 18:36:04 -0800 Subject: [PATCH 098/289] Some needed Fraction changes. --- ...tToAndInSimplestFormRuleClassifierProvider.kt | 5 +++-- ...nInputIsEquivalentToRuleClassifierProvider.kt | 4 ++-- ...onInputIsGreaterThanRuleClassifierProvider.kt | 4 ++-- ...ctionInputIsLessThanRuleClassifierProvider.kt | 4 ++-- ...hUnitsIsEquivalentToRuleClassifierProvider.kt | 4 ++-- .../android/util/math/FractionExtensions.kt | 16 +++++++++------- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt index 598d7a71ad1..af698227520 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt @@ -7,7 +7,7 @@ import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.math.approximatelyEquals -import org.oppia.android.util.math.toFloat +import org.oppia.android.util.math.toDouble import org.oppia.android.util.math.toSimplestForm import javax.inject.Inject @@ -36,6 +36,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider input: Fraction, writtenTranslationContext: WrittenTranslationContext ): Boolean { - return answer.toFloat().approximatelyEquals(input.toFloat()) && answer == input.toSimplestForm() + return answer.toDouble().approximatelyEquals(input.toDouble()) + && answer == input.toSimplestForm() } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt index 0c47a5d0657..e2c42f7ec67 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt @@ -7,7 +7,7 @@ import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.math.approximatelyEquals -import org.oppia.android.util.math.toFloat +import org.oppia.android.util.math.toDouble import javax.inject.Inject /** @@ -34,6 +34,6 @@ class FractionInputIsEquivalentToRuleClassifierProvider @Inject constructor( input: Fraction, writtenTranslationContext: WrittenTranslationContext ): Boolean { - return answer.toFloat().approximatelyEquals(input.toFloat()) + return answer.toDouble().approximatelyEquals(input.toDouble()) } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt index a44dedfcfdf..89d83f1e3d6 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt @@ -6,7 +6,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.util.math.toFloat +import org.oppia.android.util.math.toDouble import javax.inject.Inject /** @@ -33,6 +33,6 @@ class FractionInputIsGreaterThanRuleClassifierProvider @Inject constructor( input: Fraction, writtenTranslationContext: WrittenTranslationContext ): Boolean { - return answer.toFloat() > input.toFloat() + return answer.toDouble() > input.toDouble() } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt index 52d1396d9f1..02d4b9766c9 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt @@ -6,7 +6,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.util.math.toFloat +import org.oppia.android.util.math.toDouble import javax.inject.Inject /** @@ -33,6 +33,6 @@ class FractionInputIsLessThanRuleClassifierProvider @Inject constructor( input: Fraction, writtenTranslationContext: WrittenTranslationContext ): Boolean { - return answer.toFloat() < input.toFloat() + return answer.toDouble() < input.toDouble() } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt index 99bad528bb4..e94fc9191e7 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt @@ -7,7 +7,7 @@ import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.math.approximatelyEquals -import org.oppia.android.util.math.toFloat +import org.oppia.android.util.math.toDouble import javax.inject.Inject /** @@ -47,7 +47,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProvider @Inject constructor( private fun extractRealValue(number: NumberWithUnits): Double { return when (number.numberTypeCase) { NumberWithUnits.NumberTypeCase.REAL -> number.real - NumberWithUnits.NumberTypeCase.FRACTION -> number.fraction.toFloat().toDouble() + NumberWithUnits.NumberTypeCase.FRACTION -> number.fraction.toDouble() else -> throw IllegalArgumentException("Invalid number type: ${number.numberTypeCase.name}") } } diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index 69b57d5be39..d4a57faf9be 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -3,14 +3,14 @@ package org.oppia.android.util.math import org.oppia.android.app.model.Fraction /** - * Returns a float version of this fraction. + * Returns a [Double] version of this fraction. * * See: https://github.com/oppia/oppia/blob/37285a/core/templates/dev/head/domain/objects/FractionObjectFactory.ts#L73. */ -fun Fraction.toFloat(): Float { - val totalParts = ((wholeNumber * denominator) + numerator).toFloat() - val floatVal = totalParts / denominator.toFloat() - return if (isNegative) -floatVal else floatVal +fun Fraction.toDouble(): Double { + val totalParts = ((wholeNumber.toDouble() * denominator.toDouble()) + numerator.toDouble()) + val doubleVal = totalParts / denominator.toDouble() + return if (isNegative) -doubleVal else doubleVal } /** @@ -20,8 +20,10 @@ fun Fraction.toFloat(): Float { */ fun Fraction.toSimplestForm(): Fraction { val commonDenominator = gcd(numerator, denominator) - return toBuilder().setNumerator(numerator / commonDenominator) - .setDenominator(denominator / commonDenominator).build() + return toBuilder().apply { + numerator = this@toSimplestForm.numerator / commonDenominator + denominator = this@toSimplestForm.denominator / commonDenominator + }.build() } /** Returns the greatest common divisor between two integers. */ From 02e930fb825149b24eda7eacfa16fa7fe0b4a131 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 18:46:49 -0800 Subject: [PATCH 099/289] Introduce math expression + equation protos. Also adds testing libraries for both + fractions & reals (new structure). Most of this is copied from #2173. --- model/src/main/proto/math.proto | 77 +++++++ testing/BUILD.bazel | 1 + .../oppia/android/testing/math/BUILD.bazel | 75 +++++++ .../android/testing/math/FractionSubject.kt | 30 +++ .../testing/math/MathEquationSubject.kt | 21 ++ .../testing/math/MathExpressionSubject.kt | 206 ++++++++++++++++++ .../oppia/android/testing/math/RealSubject.kt | 41 ++++ 7 files changed, 451 insertions(+) create mode 100644 testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel create mode 100644 testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 0288db3148b..7dcbf780370 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -13,9 +13,86 @@ message Fraction { int32 denominator = 4; } +message Real { + oneof real_type { + Fraction rational = 1; + // Represents a decimal value. Technically these can sometimes be rational, but given IEEE-754 + // rounding errors we need to treat these values as irrational and non-factorable. + double irrational = 2; + int32 integer = 3; + } +} + // Structure containing a ratio object for eg - [1,2,3] for 1:2:3. message RatioExpression { // List of components in a ratio. It's expected that list should have more than // 1 element. repeated uint32 ratio_component = 1; } + +// Represents a mathematical expression such as 1+2. The only expression currently supported is a +// binary operation. +message MathExpression { + // TODO: document inclusive + int32 parse_start_index = 1; + // TODO: document exclusive + int32 parse_end_index = 2; + + oneof expression_type { + Real constant = 3; + string variable = 4; + MathBinaryOperation binary_operation = 5; + MathUnaryOperation unary_operation = 6; + MathFunctionCall function_call = 7; + MathExpression group = 8; + } +} + +message MathBinaryOperation { + enum Operator { + OPERATOR_UNSPECIFIED = 0; + // Represents adding two values, e.g.: 1+x. + ADD = 1; + // Represents subtracting two values, e.g.: x-2. + SUBTRACT = 2; + // Represents multiplying two values, e.g.: x*y. + MULTIPLY = 3; + // Represents dividing two values, e.g.: 1/x. + DIVIDE = 4; + // Represents taking the exponentiation of one value by another, e.g.: x^2. + EXPONENTIATE = 5; + } + + Operator operator = 1; + MathExpression left_operand = 2; + MathExpression right_operand = 3; + bool is_implicit = 4; +} + +message MathUnaryOperation { + enum Operator { + OPERATOR_UNSPECIFIED = 0; + // Represents negating a value, e.g.: -y. + NEGATE = 1; + // Represents indicating a value as positive, e.g.: +y. + POSITIVE = 2; + } + + Operator operator = 1; + MathExpression operand = 2; +} + +message MathFunctionCall { + enum FunctionType { + FUNCTION_UNSPECIFIED = 0; + SQUARE_ROOT = 1; + } + + FunctionType function_type = 1; + MathExpression argument = 2; +} + +message MathEquation { + MathExpression left_side = 1; + MathExpression right_side = 2; +} diff --git a/testing/BUILD.bazel b/testing/BUILD.bazel index 2d7d6122afc..87734adaad8 100644 --- a/testing/BUILD.bazel +++ b/testing/BUILD.bazel @@ -12,6 +12,7 @@ load("//testing:testing_test.bzl", "testing_test") # globs here to ensure new files added to migrated packages don't accidentally get included in the # top-level module library. MIGRATED_PROD_FILES = glob([ + "src/main/java/org/oppia/android/testing/math/*.kt", "src/main/java/org/oppia/android/testing/mockito/*.kt", "src/main/java/org/oppia/android/testing/networking/*.kt", "src/test/java/org/oppia/android/testing/platformparameter/*.kt", diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel new file mode 100644 index 00000000000..a1453dd4496 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -0,0 +1,75 @@ +""" +TODO: document +""" + +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +# TODO(#2747): Move these libraries to be under utility/.../math/testing. + +kt_android_library( + name = "fraction_subject", + testonly = True, + srcs = [ + "FractionSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + ], +) + +kt_android_library( + name = "math_equation_subject", + testonly = True, + srcs = [ + "MathEquationSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":math_expression_subject", + "//model/src/main/proto:math_java_proto_lite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + ], +) + +kt_android_library( + name = "math_expression_subject", + testonly = True, + srcs = [ + "MathExpressionSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":real_subject", + "//model/src/main/proto:math_java_proto_lite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + ], +) + +kt_android_library( + name = "real_subject", + testonly = True, + srcs = [ + "RealSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":fraction_subject", + "//model/src/main/proto:math_java_proto_lite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + ], +) diff --git a/testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt new file mode 100644 index 00000000000..b256d7fd555 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt @@ -0,0 +1,30 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.BooleanSubject +import com.google.common.truth.DoubleSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.Fraction +import org.oppia.android.util.math.toDouble + +class FractionSubject( + metadata: FailureMetadata, + private val actual: Fraction +) : LiteProtoSubject(metadata, actual) { + fun hasNegativePropertyThat(): BooleanSubject = assertThat(actual.isNegative) + + fun hasWholeNumberThat(): IntegerSubject = assertThat(actual.wholeNumber) + + fun hasNumeratorThat(): IntegerSubject = assertThat(actual.numerator) + + fun hasDenominatorThat(): IntegerSubject = assertThat(actual.denominator) + + fun evaluatesToRealThat(): DoubleSubject = assertThat(actual.toDouble()) + + companion object { + fun assertThat(actual: Fraction): FractionSubject = assertAbout(::FractionSubject).that(actual) + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt new file mode 100644 index 00000000000..ce24e1e08cc --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt @@ -0,0 +1,21 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.FailureMetadata +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.MathEquation +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat + +class MathEquationSubject( + metadata: FailureMetadata, + private val actual: MathEquation +) : LiteProtoSubject(metadata, actual) { + fun hasLeftHandSideThat(): MathExpressionSubject = assertThat(actual.leftSide) + + fun hasRightHandSideThat(): MathExpressionSubject = assertThat(actual.rightSide) + + companion object { + fun assertThat(actual: MathEquation): MathEquationSubject = + assertAbout(::MathEquationSubject).that(actual) + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt new file mode 100644 index 00000000000..c9be134e209 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt @@ -0,0 +1,206 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.FailureMetadata +import com.google.common.truth.StringSubject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.Real +import org.oppia.android.testing.math.RealSubject.Companion.assertThat + +// See: https://kotlinlang.org/docs/type-safe-builders.html. +class MathExpressionSubject( + metadata: FailureMetadata, + private val actual: MathExpression +) : LiteProtoSubject(metadata, actual) { + fun hasStructureThatMatches(init: ExpressionComparator.() -> Unit) { + // TODO: maybe verify that all aspects are verified? + ExpressionComparator.createFromExpression(actual).also(init) + } + + // TODO: update DSL to not have return values (since it's unnecessary). + @ExpressionComparatorMarker + class ExpressionComparator private constructor(private val expression: MathExpression) { + // TODO: convert to constant comparator? + fun constant(init: ConstantComparator.() -> Unit): ConstantComparator = + ConstantComparator.createFromExpression(expression).also(init) + + fun variable(init: VariableComparator.() -> Unit): VariableComparator = + VariableComparator.createFromExpression(expression).also(init) + + fun addition(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.ADD + ).also(init) + } + + fun subtraction(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.SUBTRACT + ).also(init) + } + + fun multiplication(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.MULTIPLY + ).also(init) + } + + fun division(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.DIVIDE + ).also(init) + } + + fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.EXPONENTIATE + ).also(init) + } + + fun negation(init: UnaryOperationComparator.() -> Unit): UnaryOperationComparator { + return UnaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathUnaryOperation.Operator.NEGATE + ).also(init) + } + + fun positive(init: UnaryOperationComparator.() -> Unit): UnaryOperationComparator { + return UnaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathUnaryOperation.Operator.POSITIVE + ).also(init) + } + + fun functionCallTo( + type: MathFunctionCall.FunctionType, + init: FunctionCallComparator.() -> Unit + ): FunctionCallComparator { + return FunctionCallComparator.createFromExpression( + expression, + expectedFunctionType = type + ).also(init) + } + + fun group(init: ExpressionComparator.() -> Unit): ExpressionComparator { + return createFromExpression(expression.group).also(init) + } + + internal companion object { + fun createFromExpression(expression: MathExpression): ExpressionComparator = + ExpressionComparator(expression) + } + } + + @ExpressionComparatorMarker + class ConstantComparator private constructor(private val constant: Real) { + fun withValueThat(): RealSubject = assertThat(constant) + + internal companion object { + fun createFromExpression(expression: MathExpression): ConstantComparator { + assertThat(expression.expressionTypeCase).isEqualTo(CONSTANT) + return ConstantComparator(expression.constant) + } + } + } + + @ExpressionComparatorMarker + class VariableComparator private constructor(private val variableName: String) { + fun withNameThat(): StringSubject = assertThat(variableName) + + internal companion object { + fun createFromExpression(expression: MathExpression): VariableComparator { + assertThat(expression.expressionTypeCase).isEqualTo(VARIABLE) + return VariableComparator(expression.variable) + } + } + } + + @ExpressionComparatorMarker + class BinaryOperationComparator private constructor( + private val operation: MathBinaryOperation + ) { + fun leftOperand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(operation.leftOperand).also(init) + + fun rightOperand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(operation.rightOperand).also(init) + + internal companion object { + fun createFromExpression( + expression: MathExpression, + expectedOperator: MathBinaryOperation.Operator + ): BinaryOperationComparator { + assertThat(expression.expressionTypeCase).isEqualTo(BINARY_OPERATION) + assertWithMessage("Expected binary operation with operator: $expectedOperator") + .that(expression.binaryOperation.operator) + .isEqualTo(expectedOperator) + return BinaryOperationComparator(expression.binaryOperation) + } + } + } + + @ExpressionComparatorMarker + class UnaryOperationComparator private constructor( + private val operation: MathUnaryOperation + ) { + fun operand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(operation.operand).also(init) + + internal companion object { + fun createFromExpression( + expression: MathExpression, + expectedOperator: MathUnaryOperation.Operator + ): UnaryOperationComparator { + assertThat(expression.expressionTypeCase).isEqualTo(UNARY_OPERATION) + assertWithMessage("Expected unary operation with operator: $expectedOperator") + .that(expression.unaryOperation.operator) + .isEqualTo(expectedOperator) + return UnaryOperationComparator(expression.unaryOperation) + } + } + } + + @ExpressionComparatorMarker + class FunctionCallComparator private constructor( + private val functionCall: MathFunctionCall + ) { + fun argument(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(functionCall.argument).also(init) + + internal companion object { + fun createFromExpression( + expression: MathExpression, + expectedFunctionType: MathFunctionCall.FunctionType + ): FunctionCallComparator { + assertThat(expression.expressionTypeCase).isEqualTo(FUNCTION_CALL) + assertWithMessage("Expected function call to: $expectedFunctionType") + .that(expression.functionCall.functionType) + .isEqualTo(expectedFunctionType) + return FunctionCallComparator(expression.functionCall) + } + } + } + + companion object { + @DslMarker private annotation class ExpressionComparatorMarker + + fun assertThat(actual: MathExpression): MathExpressionSubject = + assertAbout(::MathExpressionSubject).that(actual) + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt new file mode 100644 index 00000000000..8f9edddda0b --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt @@ -0,0 +1,41 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.DoubleSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.Real +import org.oppia.android.testing.math.FractionSubject.Companion.assertThat + +class RealSubject( + metadata: FailureMetadata, + private val actual: Real +) : LiteProtoSubject(metadata, actual) { + fun isRationalThat(): FractionSubject { + verifyTypeToBe(Real.RealTypeCase.RATIONAL) + return assertThat(actual.rational) + } + + fun isIrrationalThat(): DoubleSubject { + verifyTypeToBe(Real.RealTypeCase.IRRATIONAL) + return assertThat(actual.irrational) + } + + fun isIntegerThat(): IntegerSubject { + verifyTypeToBe(Real.RealTypeCase.INTEGER) + return assertThat(actual.integer) + } + + private fun verifyTypeToBe(expected: Real.RealTypeCase) { + assertWithMessage("Expected real type to be $expected, not: ${actual.realTypeCase}") + .that(actual.realTypeCase) + .isEqualTo(expected) + } + + companion object { + fun assertThat(actual: Real): RealSubject = assertAbout(::RealSubject).that(actual) + } +} From 1ecc48e6a730fdfa2892c51ea8974a9eeb4ca958 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 19:14:08 -0800 Subject: [PATCH 100/289] Migrate tests to shared test utilities. This applies to math expressions, equations, fractions, and reals. --- .../testing/math/MathEquationSubject.kt | 3 +- .../testing/math/MathExpressionSubject.kt | 3 +- .../org/oppia/android/util/math/BUILD.bazel | 4 + .../util/math/MathExpressionParserTest.kt | 320 +++--------------- 4 files changed, 48 insertions(+), 282 deletions(-) diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt index ce24e1e08cc..20e33458b5c 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt @@ -8,7 +8,8 @@ import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat class MathEquationSubject( metadata: FailureMetadata, - private val actual: MathEquation + // TODO: restrict visibility. + val actual: MathEquation ) : LiteProtoSubject(metadata, actual) { fun hasLeftHandSideThat(): MathExpressionSubject = assertThat(actual.leftSide) diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt index c9be134e209..acd537a1e21 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt @@ -21,7 +21,8 @@ import org.oppia.android.testing.math.RealSubject.Companion.assertThat // See: https://kotlinlang.org/docs/type-safe-builders.html. class MathExpressionSubject( metadata: FailureMetadata, - private val actual: MathExpression + // TODO: restrict visibility. + val actual: MathExpression ) : LiteProtoSubject(metadata, actual) { fun hasStructureThatMatches(init: ExpressionComparator.() -> Unit) { // TODO: maybe verify that all aspects are verified? diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index aa91e9097b7..61e76583c5b 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -12,6 +12,10 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:fraction_subject", + "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", + "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", + "//testing/src/main/java/org/oppia/android/testing/math:real_subject", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_extensions_truth-liteproto-extension", "//third_party:com_google_truth_truth", diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index 83be344b1c0..90444d30dab 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -10,6 +10,7 @@ import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import com.google.common.truth.extensions.proto.LiteProtoSubject +import kotlin.math.sqrt import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.ComparableOperationList @@ -17,18 +18,10 @@ import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulati import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION import org.oppia.android.app.model.ComparableOperationList.ComparableOperation import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase -import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.MathBinaryOperation import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE -import org.oppia.android.app.model.MathFunctionCall import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT -import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.OppiaLanguage import org.oppia.android.app.model.OppiaLanguage.ARABIC import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE @@ -40,6 +33,14 @@ import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Real +import org.oppia.android.testing.math.FractionSubject +import org.oppia.android.testing.math.FractionSubject.Companion.assertThat +import org.oppia.android.testing.math.MathEquationSubject +import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat +import org.oppia.android.testing.math.MathExpressionSubject +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.testing.math.RealSubject +import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError @@ -65,7 +66,6 @@ import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError import org.robolectric.annotation.LooperMode -import kotlin.math.sqrt /** Tests for [MathExpressionParser]. */ @RunWith(AndroidJUnit4::class) @@ -7133,240 +7133,53 @@ class MathExpressionParserTest { private fun parsePolynomialFromAlgebraicExpression(expression: String) = parseAlgebraicExpressionWithAllErrors(expression).toPolynomial() - @DslMarker private annotation class ExpressionComparatorMarker - - @DslMarker private annotation class ComparableOperationComparatorMarker // See: https://kotlinlang.org/docs/type-safe-builders.html. - private class MathExpressionSubject( - metadata: FailureMetadata, - private val actual: MathExpression - ) : LiteProtoSubject(metadata, actual) { - fun hasStructureThatMatches(init: ExpressionComparator.() -> Unit) { - // TODO: maybe verify that all aspects are verified? - ExpressionComparator.createFromExpression(actual).also(init) - } - - fun evaluatesToRationalThat(): FractionSubject = - assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.RATIONAL).rational) - - fun evaluatesToIrrationalThat(): DoubleSubject = - assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.IRRATIONAL).irrational) - - fun evaluatesToIntegerThat(): IntegerSubject = - assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.INTEGER).integer) - - fun convertsToLatexStringThat(): StringSubject = - assertThat(convertToLatex(divAsFraction = false)) - - fun convertsWithFractionsToLatexStringThat(): StringSubject = - assertThat(convertToLatex(divAsFraction = true)) - - fun forHumanReadable(language: OppiaLanguage): HumanReadableStringChecker = - HumanReadableStringChecker(language, actual::toHumanReadableString) - - private fun evaluateAsReal(expectedType: Real.RealTypeCase): Real { - val real = actual.evaluateAsNumericExpression() - assertWithMessage("Failed to evaluate numeric expression").that(real).isNotNull() - assertWithMessage("Expected constant to evaluate to $expectedType") - .that(real?.realTypeCase) - .isEqualTo(expectedType) - return checkNotNull(real) // Just to remove the nullable operator; the actual check is above. - } - - private fun convertToLatex(divAsFraction: Boolean): String = actual.toRawLatex(divAsFraction) - - // TODO: update DSL to not have return values (since it's unnecessary). - @ExpressionComparatorMarker - class ExpressionComparator private constructor(private val expression: MathExpression) { - // TODO: convert to constant comparator? - fun constant(init: ConstantComparator.() -> Unit): ConstantComparator = - ConstantComparator.createFromExpression(expression).also(init) - - fun variable(init: VariableComparator.() -> Unit): VariableComparator = - VariableComparator.createFromExpression(expression).also(init) - - fun addition(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( - expression, - expectedOperator = MathBinaryOperation.Operator.ADD - ).also(init) - } - - fun subtraction(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( - expression, - expectedOperator = MathBinaryOperation.Operator.SUBTRACT - ).also(init) - } - - fun multiplication(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( - expression, - expectedOperator = MathBinaryOperation.Operator.MULTIPLY - ).also(init) - } - - fun division(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( - expression, - expectedOperator = MathBinaryOperation.Operator.DIVIDE - ).also(init) - } - - fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( - expression, - expectedOperator = MathBinaryOperation.Operator.EXPONENTIATE - ).also(init) - } - - fun negation(init: UnaryOperationComparator.() -> Unit): UnaryOperationComparator { - return UnaryOperationComparator.createFromExpression( - expression, - expectedOperator = MathUnaryOperation.Operator.NEGATE - ).also(init) - } - - fun positive(init: UnaryOperationComparator.() -> Unit): UnaryOperationComparator { - return UnaryOperationComparator.createFromExpression( - expression, - expectedOperator = MathUnaryOperation.Operator.POSITIVE - ).also(init) - } - - fun functionCallTo( - type: MathFunctionCall.FunctionType, - init: FunctionCallComparator.() -> Unit - ): FunctionCallComparator { - return FunctionCallComparator.createFromExpression( - expression, - expectedFunctionType = type - ).also(init) - } - - fun group(init: ExpressionComparator.() -> Unit): ExpressionComparator { - return createFromExpression(expression.group).also(init) - } - - internal companion object { - fun createFromExpression(expression: MathExpression): ExpressionComparator = - ExpressionComparator(expression) - } - } - - @ExpressionComparatorMarker - class ConstantComparator private constructor(private val constant: Real) { - fun withValueThat(): RealSubject = assertThat(constant) - - internal companion object { - fun createFromExpression(expression: MathExpression): ConstantComparator { - assertThat(expression.expressionTypeCase).isEqualTo(CONSTANT) - return ConstantComparator(expression.constant) - } - } - } + @DslMarker private annotation class ComparableOperationComparatorMarker - @ExpressionComparatorMarker - class VariableComparator private constructor(private val variableName: String) { - fun withNameThat(): StringSubject = assertThat(variableName) + // TODO: move these to MathExpressionSubject + fun MathExpressionSubject.evaluatesToRationalThat(): FractionSubject = + assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.RATIONAL).rational) - internal companion object { - fun createFromExpression(expression: MathExpression): VariableComparator { - assertThat(expression.expressionTypeCase).isEqualTo(VARIABLE) - return VariableComparator(expression.variable) - } - } - } + fun MathExpressionSubject.evaluatesToIrrationalThat(): DoubleSubject = + assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.IRRATIONAL).irrational) - @ExpressionComparatorMarker - class BinaryOperationComparator private constructor( - private val operation: MathBinaryOperation - ) { - fun leftOperand(init: ExpressionComparator.() -> Unit): ExpressionComparator = - ExpressionComparator.createFromExpression(operation.leftOperand).also(init) + fun MathExpressionSubject.evaluatesToIntegerThat(): IntegerSubject = + assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.INTEGER).integer) - fun rightOperand(init: ExpressionComparator.() -> Unit): ExpressionComparator = - ExpressionComparator.createFromExpression(operation.rightOperand).also(init) + fun MathExpressionSubject.convertsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = false)) - internal companion object { - fun createFromExpression( - expression: MathExpression, - expectedOperator: MathBinaryOperation.Operator - ): BinaryOperationComparator { - assertThat(expression.expressionTypeCase).isEqualTo(BINARY_OPERATION) - assertWithMessage("Expected binary operation with operator: $expectedOperator") - .that(expression.binaryOperation.operator) - .isEqualTo(expectedOperator) - return BinaryOperationComparator(expression.binaryOperation) - } - } - } + fun MathExpressionSubject.convertsWithFractionsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = true)) - @ExpressionComparatorMarker - class UnaryOperationComparator private constructor( - private val operation: MathUnaryOperation - ) { - fun operand(init: ExpressionComparator.() -> Unit): ExpressionComparator = - ExpressionComparator.createFromExpression(operation.operand).also(init) + fun MathExpressionSubject.forHumanReadable(language: OppiaLanguage): HumanReadableStringChecker = + HumanReadableStringChecker(language, actual::toHumanReadableString) - internal companion object { - fun createFromExpression( - expression: MathExpression, - expectedOperator: MathUnaryOperation.Operator - ): UnaryOperationComparator { - assertThat(expression.expressionTypeCase).isEqualTo(UNARY_OPERATION) - assertWithMessage("Expected unary operation with operator: $expectedOperator") - .that(expression.unaryOperation.operator) - .isEqualTo(expectedOperator) - return UnaryOperationComparator(expression.unaryOperation) - } - } - } - - @ExpressionComparatorMarker - class FunctionCallComparator private constructor( - private val functionCall: MathFunctionCall - ) { - fun argument(init: ExpressionComparator.() -> Unit): ExpressionComparator = - ExpressionComparator.createFromExpression(functionCall.argument).also(init) - - internal companion object { - fun createFromExpression( - expression: MathExpression, - expectedFunctionType: MathFunctionCall.FunctionType - ): FunctionCallComparator { - assertThat(expression.expressionTypeCase).isEqualTo(FUNCTION_CALL) - assertWithMessage("Expected function call to: $expectedFunctionType") - .that(expression.functionCall.functionType) - .isEqualTo(expectedFunctionType) - return FunctionCallComparator(expression.functionCall) - } - } - } + private fun MathExpressionSubject.evaluateAsReal(expectedType: Real.RealTypeCase): Real { + val real = actual.evaluateAsNumericExpression() + assertWithMessage("Failed to evaluate numeric expression").that(real).isNotNull() + assertWithMessage("Expected constant to evaluate to $expectedType") + .that(real?.realTypeCase) + .isEqualTo(expectedType) + return checkNotNull(real) // Just to remove the nullable operator; the actual check is above. } - private class MathEquationSubject( - metadata: FailureMetadata, - private val actual: MathEquation - ) : LiteProtoSubject(metadata, actual) { - fun hasLeftHandSideThat(): MathExpressionSubject = assertThat(actual.leftSide) - - fun hasRightHandSideThat(): MathExpressionSubject = assertThat(actual.rightSide) + private fun MathExpressionSubject.convertToLatex(divAsFraction: Boolean): String = actual.toRawLatex(divAsFraction) - fun convertsToLatexStringThat(): StringSubject = - assertThat(convertToLatex(divAsFraction = false)) + // TODO: move these to MathEquationSubject + fun MathEquationSubject.convertsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = false)) - fun convertsWithFractionsToLatexStringThat(): StringSubject = - assertThat(convertToLatex(divAsFraction = true)) + fun MathEquationSubject.convertsWithFractionsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = true)) - fun forHumanReadable(language: OppiaLanguage): HumanReadableStringChecker = - HumanReadableStringChecker(language, actual::toHumanReadableString) + fun MathEquationSubject.forHumanReadable(language: OppiaLanguage): HumanReadableStringChecker = + HumanReadableStringChecker(language, actual::toHumanReadableString) - private fun convertToLatex(divAsFraction: Boolean): String = actual.toRawLatex(divAsFraction) - } + private fun MathEquationSubject.convertToLatex(divAsFraction: Boolean): String = actual.toRawLatex(divAsFraction) - private class HumanReadableStringChecker( + class HumanReadableStringChecker( private val language: OppiaLanguage, private val maybeConvertToHumanReadableString: (OppiaLanguage, Boolean) -> String? ) { @@ -7392,48 +7205,6 @@ class MathExpressionParserTest { } } - // TODO: move these to a common location. - private class FractionSubject( - metadata: FailureMetadata, - private val actual: Fraction - ) : LiteProtoSubject(metadata, actual) { - fun hasNegativePropertyThat(): BooleanSubject = assertThat(actual.isNegative) - - fun hasWholeNumberThat(): IntegerSubject = assertThat(actual.wholeNumber) - - fun hasNumeratorThat(): IntegerSubject = assertThat(actual.numerator) - - fun hasDenominatorThat(): IntegerSubject = assertThat(actual.denominator) - - fun evaluatesToRealThat(): DoubleSubject = assertThat(actual.toDouble()) - } - - private class RealSubject( - metadata: FailureMetadata, - private val actual: Real - ) : LiteProtoSubject(metadata, actual) { - fun isRationalThat(): FractionSubject { - verifyTypeToBe(Real.RealTypeCase.RATIONAL) - return assertThat(actual.rational) - } - - fun isIrrationalThat(): DoubleSubject { - verifyTypeToBe(Real.RealTypeCase.IRRATIONAL) - return assertThat(actual.irrational) - } - - fun isIntegerThat(): IntegerSubject { - verifyTypeToBe(Real.RealTypeCase.INTEGER) - return assertThat(actual.integer) - } - - private fun verifyTypeToBe(expected: Real.RealTypeCase) { - assertWithMessage("Expected real type to be $expected, not: ${actual.realTypeCase}") - .that(actual.realTypeCase) - .isEqualTo(expected) - } - } - private class ComparableOperationListSubject( metadata: FailureMetadata, private val actual: ComparableOperationList @@ -7737,17 +7508,6 @@ class MathExpressionParserTest { ) } - private fun assertThat(actual: MathExpression): MathExpressionSubject = - assertAbout(::MathExpressionSubject).that(actual) - - private fun assertThat(actual: MathEquation): MathEquationSubject = - assertAbout(::MathEquationSubject).that(actual) - - private fun assertThat(actual: Fraction): FractionSubject = - assertAbout(::FractionSubject).that(actual) - - private fun assertThat(actual: Real): RealSubject = assertAbout(::RealSubject).that(actual) - private fun assertThat(actual: ComparableOperationList): ComparableOperationListSubject = assertAbout(::ComparableOperationListSubject).that(actual) From e45635d0adbd38c5f44521323e4b8f5329f3b8f3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 19:20:25 -0800 Subject: [PATCH 101/289] Add protos + testing lib for commutative exprs. --- model/src/main/proto/math.proto | 53 ++++++ .../oppia/android/testing/math/BUILD.bazel | 17 ++ .../math/ComparableOperationListSubject.kt | 172 ++++++++++++++++++ 3 files changed, 242 insertions(+) create mode 100644 testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 7dcbf780370..168db946cd6 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -96,3 +96,56 @@ message MathEquation { MathExpression left_side = 1; MathExpression right_side = 2; } + +// Represents a list of comparable mathematics operations. 'Comparable' here means that this +// structure provides a trivial way to compare commutative operations (i.e. by extracting terms from +// multiple subsequent commutative operations into lists that can be deterministically sorted). This +// structure is meant to provide a means to compare two expressions without considering +// associativity or commutativity (though the latter requires the operation lists stored within this +// structure to be sorted before using standard proto equals checking). +message ComparableOperationList { + message ComparableOperation { + // Treat this operation (e.g. x) as negated (e.g. -x). + bool is_negated = 1; + + // Treat this operation (e.g. x) as a multiplicative inverse (e.g. 1/x). + bool is_inverted = 2; + + oneof comparison_type { + CommutativeAccumulation commutative_accumulation = 3; + NonCommutativeOperation non_commutative_operation = 4; + Real constant_term = 5; + string variable_term = 6; + } + } + // Represents an accumulation of operations (such as a summation or product). This helps simplify + // comparison across commutative boundaries by collecting terms into sortable lists, such as the + // expression 1+2+3 becoming [1,2,3] and trivially comparable to [3,2,1] from 3+2+1. + // + // Subsequent subtractions are treated as additions with each term arithmetically negated (i.e. + // f(x)=-x). Similarly, divisions are considered multiplications with each divisor being + // multiplicatively inverted (i.e. the reciprocal function: f(x)=1/x). + message CommutativeAccumulation { + enum AccumulationType { + ACCUMULATION_TYPE_UNSPECIFIED = 0; + SUMMATION = 1; + PRODUCT = 2; + } + + AccumulationType accumulation_type = 1; + repeated ComparableOperation combined_operations = 2; + } + message NonCommutativeOperation { + oneof operation_type { + BinaryOperation exponentiation = 1; + ComparableOperation square_root = 2; + } + + message BinaryOperation { + ComparableOperation left_operand = 1; + ComparableOperation right_operand = 2; + } + } + + ComparableOperation root_operation = 1; +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel index a1453dd4496..ed7e885c3c0 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -6,6 +6,23 @@ load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") # TODO(#2747): Move these libraries to be under utility/.../math/testing. +kt_android_library( + name = "comparable_operation_list_subject", + testonly = True, + srcs = [ + "ComparableOperationListSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":real_subject", + "//model/src/main/proto:math_java_proto_lite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + ], +) + kt_android_library( name = "fraction_subject", testonly = True, diff --git a/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt new file mode 100644 index 00000000000..ce32ec28d2b --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt @@ -0,0 +1,172 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.BooleanSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.StringSubject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase +import org.oppia.android.app.model.Real +import org.oppia.android.testing.math.RealSubject.Companion.assertThat + +class ComparableOperationListSubject( + metadata: FailureMetadata, + private val actual: ComparableOperationList +) : LiteProtoSubject(metadata, actual) { + fun hasStructureThatMatches(init: ComparableOperationComparator.() -> Unit) { + ComparableOperationComparator.createFrom(actual.rootOperation).also(init) + } + + @ComparableOperationComparatorMarker + class ComparableOperationComparator private constructor( + private val operation: ComparableOperation + ) { + fun hasNegatedPropertyThat(): BooleanSubject = assertThat(operation.isNegated) + + fun hasInvertedPropertyThat(): BooleanSubject = assertThat(operation.isInverted) + + fun commutativeAccumulationWithType( + type: ComparableOperationList.CommutativeAccumulation.AccumulationType, + init: CommutativeAccumulationComparator.() -> Unit + ): CommutativeAccumulationComparator = + CommutativeAccumulationComparator.createFrom(type, operation).also(init) + + fun nonCommutativeOperation( + init: NonCommutativeOperationComparator.() -> Unit + ): NonCommutativeOperationComparator = + NonCommutativeOperationComparator.createFrom(operation).also(init) + + fun constantTerm(init: ConstantTermComparator.() -> Unit): ConstantTermComparator = + ConstantTermComparator.createFrom(operation).also(init) + + fun variableTerm(init: VariableTermComparator.() -> Unit): VariableTermComparator = + VariableTermComparator.createFrom(operation).also(init) + + internal companion object { + fun createFrom(operation: ComparableOperation): ComparableOperationComparator = + ComparableOperationComparator(operation) + } + } + + @ComparableOperationComparatorMarker + class CommutativeAccumulationComparator private constructor( + private val accumulation: ComparableOperationList.CommutativeAccumulation + ) { + fun hasOperandCountThat(): IntegerSubject = assertThat(accumulation.combinedOperationsCount) + + fun index( + index: Int, + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator { + return ComparableOperationComparator.createFrom( + accumulation.combinedOperationsList[index] + ).also(init) + } + + internal companion object { + fun createFrom( + type: ComparableOperationList.CommutativeAccumulation.AccumulationType, + operation: ComparableOperation + ): CommutativeAccumulationComparator { + assertThat(operation.comparisonTypeCase) + .isEqualTo(ComparisonTypeCase.COMMUTATIVE_ACCUMULATION) + assertThat(operation.commutativeAccumulation.accumulationType).isEqualTo(type) + return CommutativeAccumulationComparator(operation.commutativeAccumulation) + } + } + } + + @ComparableOperationComparatorMarker + class NonCommutativeOperationComparator private constructor( + private val operation: ComparableOperationList.NonCommutativeOperation + ) { + fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + verifyTypeAs( + ComparableOperationList.NonCommutativeOperation.OperationTypeCase.EXPONENTIATION + ) + return BinaryOperationComparator.createFrom(operation.exponentiation).also(init) + } + + fun squareRootWithArgument( + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator { + verifyTypeAs(ComparableOperationList.NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT) + return ComparableOperationComparator.createFrom(operation.squareRoot).also(init) + } + + private fun verifyTypeAs( + type: ComparableOperationList.NonCommutativeOperation.OperationTypeCase + ) { + assertThat(operation.operationTypeCase).isEqualTo(type) + } + + internal companion object { + fun createFrom(operation: ComparableOperation): NonCommutativeOperationComparator { + assertThat(operation.comparisonTypeCase) + .isEqualTo(ComparisonTypeCase.NON_COMMUTATIVE_OPERATION) + return NonCommutativeOperationComparator(operation.nonCommutativeOperation) + } + } + } + + @ComparableOperationComparatorMarker + class BinaryOperationComparator private constructor( + private val operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation + ) { + fun leftOperand( + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator = + ComparableOperationComparator.createFrom(operation.leftOperand).also(init) + + fun rightOperand( + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator = + ComparableOperationComparator.createFrom(operation.rightOperand).also(init) + + internal companion object { + fun createFrom( + operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation + ): BinaryOperationComparator = BinaryOperationComparator(operation) + } + } + + @ComparableOperationComparatorMarker + class ConstantTermComparator private constructor( + private val constant: Real + ) { + fun withValueThat(): RealSubject = assertThat(constant) + + internal companion object { + fun createFrom(operation: ComparableOperation): ConstantTermComparator { + assertThat(operation.comparisonTypeCase).isEqualTo(ComparisonTypeCase.CONSTANT_TERM) + return ConstantTermComparator(operation.constantTerm) + } + } + } + + @ComparableOperationComparatorMarker + class VariableTermComparator private constructor( + private val variableName: String + ) { + fun withNameThat(): StringSubject = assertThat(variableName) + + internal companion object { + fun createFrom(operation: ComparableOperation): VariableTermComparator { + assertThat(operation.comparisonTypeCase).isEqualTo(ComparisonTypeCase.VARIABLE_TERM) + return VariableTermComparator(operation.variableTerm) + } + } + } + + companion object { + // See: https://kotlinlang.org/docs/type-safe-builders.html. + @DslMarker private annotation class ComparableOperationComparatorMarker + + fun assertThat(actual: ComparableOperationList): ComparableOperationListSubject = + assertAbout(::ComparableOperationListSubject).that(actual) + } +} From aaceb22ecdd53a08d80e4642d2413e2cf9615e99 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 19:26:45 -0800 Subject: [PATCH 102/289] Migrate tests to shared test utilities. This applies to ComparableOperationList (which introduces support for commutative comparisons of math expressions). --- .../org/oppia/android/util/math/BUILD.bazel | 1 + .../util/math/MathExpressionParserTest.kt | 159 +----------------- 2 files changed, 3 insertions(+), 157 deletions(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 61e76583c5b..8f1e4145718 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -12,6 +12,7 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_list_subject", "//testing/src/main/java/org/oppia/android/testing/math:fraction_subject", "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index 90444d30dab..594c4f718cf 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -33,6 +33,8 @@ import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Real +import org.oppia.android.testing.math.ComparableOperationListSubject +import org.oppia.android.testing.math.ComparableOperationListSubject.Companion.assertThat import org.oppia.android.testing.math.FractionSubject import org.oppia.android.testing.math.FractionSubject.Companion.assertThat import org.oppia.android.testing.math.MathEquationSubject @@ -7133,10 +7135,6 @@ class MathExpressionParserTest { private fun parsePolynomialFromAlgebraicExpression(expression: String) = parseAlgebraicExpressionWithAllErrors(expression).toPolynomial() - - // See: https://kotlinlang.org/docs/type-safe-builders.html. - @DslMarker private annotation class ComparableOperationComparatorMarker - // TODO: move these to MathExpressionSubject fun MathExpressionSubject.evaluatesToRationalThat(): FractionSubject = assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.RATIONAL).rational) @@ -7205,156 +7203,6 @@ class MathExpressionParserTest { } } - private class ComparableOperationListSubject( - metadata: FailureMetadata, - private val actual: ComparableOperationList - ) : LiteProtoSubject(metadata, actual) { - fun hasStructureThatMatches(init: ComparableOperationComparator.() -> Unit) { - ComparableOperationComparator.createFrom(actual.rootOperation).also(init) - } - - @ComparableOperationComparatorMarker - class ComparableOperationComparator private constructor( - private val operation: ComparableOperation - ) { - fun hasNegatedPropertyThat(): BooleanSubject = assertThat(operation.isNegated) - - fun hasInvertedPropertyThat(): BooleanSubject = assertThat(operation.isInverted) - - fun commutativeAccumulationWithType( - type: ComparableOperationList.CommutativeAccumulation.AccumulationType, - init: CommutativeAccumulationComparator.() -> Unit - ): CommutativeAccumulationComparator = - CommutativeAccumulationComparator.createFrom(type, operation).also(init) - - fun nonCommutativeOperation( - init: NonCommutativeOperationComparator.() -> Unit - ): NonCommutativeOperationComparator = - NonCommutativeOperationComparator.createFrom(operation).also(init) - - fun constantTerm(init: ConstantTermComparator.() -> Unit): ConstantTermComparator = - ConstantTermComparator.createFrom(operation).also(init) - - fun variableTerm(init: VariableTermComparator.() -> Unit): VariableTermComparator = - VariableTermComparator.createFrom(operation).also(init) - - internal companion object { - fun createFrom(operation: ComparableOperation): ComparableOperationComparator = - ComparableOperationComparator(operation) - } - } - - @ComparableOperationComparatorMarker - class CommutativeAccumulationComparator private constructor( - private val accumulation: ComparableOperationList.CommutativeAccumulation - ) { - fun hasOperandCountThat(): IntegerSubject = assertThat(accumulation.combinedOperationsCount) - - fun index( - index: Int, - init: ComparableOperationComparator.() -> Unit - ): ComparableOperationComparator { - return ComparableOperationComparator.createFrom( - accumulation.combinedOperationsList[index] - ).also(init) - } - - internal companion object { - fun createFrom( - type: ComparableOperationList.CommutativeAccumulation.AccumulationType, - operation: ComparableOperation - ): CommutativeAccumulationComparator { - assertThat(operation.comparisonTypeCase) - .isEqualTo(ComparisonTypeCase.COMMUTATIVE_ACCUMULATION) - assertThat(operation.commutativeAccumulation.accumulationType).isEqualTo(type) - return CommutativeAccumulationComparator(operation.commutativeAccumulation) - } - } - } - - @ComparableOperationComparatorMarker - class NonCommutativeOperationComparator private constructor( - private val operation: ComparableOperationList.NonCommutativeOperation - ) { - fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - verifyTypeAs( - ComparableOperationList.NonCommutativeOperation.OperationTypeCase.EXPONENTIATION - ) - return BinaryOperationComparator.createFrom(operation.exponentiation).also(init) - } - - fun squareRootWithArgument( - init: ComparableOperationComparator.() -> Unit - ): ComparableOperationComparator { - verifyTypeAs(ComparableOperationList.NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT) - return ComparableOperationComparator.createFrom(operation.squareRoot).also(init) - } - - private fun verifyTypeAs( - type: ComparableOperationList.NonCommutativeOperation.OperationTypeCase - ) { - assertThat(operation.operationTypeCase).isEqualTo(type) - } - - internal companion object { - fun createFrom(operation: ComparableOperation): NonCommutativeOperationComparator { - assertThat(operation.comparisonTypeCase) - .isEqualTo(ComparisonTypeCase.NON_COMMUTATIVE_OPERATION) - return NonCommutativeOperationComparator(operation.nonCommutativeOperation) - } - } - } - - @ComparableOperationComparatorMarker - class BinaryOperationComparator private constructor( - private val operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation - ) { - fun leftOperand( - init: ComparableOperationComparator.() -> Unit - ): ComparableOperationComparator = - ComparableOperationComparator.createFrom(operation.leftOperand).also(init) - - fun rightOperand( - init: ComparableOperationComparator.() -> Unit - ): ComparableOperationComparator = - ComparableOperationComparator.createFrom(operation.rightOperand).also(init) - - internal companion object { - fun createFrom( - operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation - ): BinaryOperationComparator = BinaryOperationComparator(operation) - } - } - - @ComparableOperationComparatorMarker - class ConstantTermComparator private constructor( - private val constant: Real - ) { - fun withValueThat(): RealSubject = assertThat(constant) - - internal companion object { - fun createFrom(operation: ComparableOperation): ConstantTermComparator { - assertThat(operation.comparisonTypeCase).isEqualTo(ComparisonTypeCase.CONSTANT_TERM) - return ConstantTermComparator(operation.constantTerm) - } - } - } - - @ComparableOperationComparatorMarker - class VariableTermComparator private constructor( - private val variableName: String - ) { - fun withNameThat(): StringSubject = assertThat(variableName) - - internal companion object { - fun createFrom(operation: ComparableOperation): VariableTermComparator { - assertThat(operation.comparisonTypeCase).isEqualTo(ComparisonTypeCase.VARIABLE_TERM) - return VariableTermComparator(operation.variableTerm) - } - } - } - } - private class PolynomialSubject( metadata: FailureMetadata, private val actual: Polynomial? @@ -7508,9 +7356,6 @@ class MathExpressionParserTest { ) } - private fun assertThat(actual: ComparableOperationList): ComparableOperationListSubject = - assertAbout(::ComparableOperationListSubject).that(actual) - private fun assertThat(actual: Polynomial?): PolynomialSubject = assertAbout(::PolynomialSubject).that(actual) From da5d72d1cc5bf3e7a1493be452a40ec3d4bc06b9 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 19:50:42 -0800 Subject: [PATCH 103/289] Add protos & test libs for polynomials. --- .../util/InteractionObjectExtensions.kt | 9 --- model/src/main/proto/math.proto | 14 ++++ .../oppia/android/testing/math/BUILD.bazel | 18 +++++ .../android/testing/math/PolynomialSubject.kt | 79 +++++++++++++++++++ .../org/oppia/android/util/math/BUILD.bazel | 2 + .../android/util/math/FloatExtensions.kt | 2 + .../android/util/math/FractionExtensions.kt | 59 ++++++++++++++ .../android/util/math/PolynomialExtensions.kt | 56 +++++++++++++ .../oppia/android/util/math/RealExtensions.kt | 53 +++++++++++++ 9 files changed, 283 insertions(+), 9 deletions(-) create mode 100644 testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt diff --git a/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt b/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt index 2e37e34e0f7..11d3af018a9 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt +++ b/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt @@ -107,15 +107,6 @@ private fun ImageWithRegions.toAnswerString(): String = private fun ClickOnImage.toAnswerString(): String = "[(${clickedRegionsList.joinToString()}), (${clickPosition.x}, ${clickPosition.y})]" -// https://github.com/oppia/oppia/blob/37285a/core/templates/dev/head/domain/objects/FractionObjectFactory.ts#L47 -private fun Fraction.toAnswerString(): String { - val fractionString = if (numerator != 0) "$numerator/$denominator" else "" - val mixedString = if (wholeNumber != 0) "$wholeNumber $fractionString" else "" - val positiveFractionString = if (mixedString.isNotEmpty()) mixedString else fractionString - val negativeString = if (isNegative) "-" else "" - return if (positiveFractionString.isNotEmpty()) "$negativeString$positiveFractionString" else "0" -} - private fun TranslatableHtmlContentId.toAnswerString(): String { return "content_id=$contentId" } diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 168db946cd6..95127692db6 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -149,3 +149,17 @@ message ComparableOperationList { ComparableOperation root_operation = 1; } + +message Polynomial { + repeated Term term = 1; + + message Term { + Real coefficient = 1; + repeated Variable variable = 2; + + message Variable { + string name = 1; + uint32 power = 2; + } + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel index ed7e885c3c0..275084d309b 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -74,6 +74,24 @@ kt_android_library( ], ) +kt_android_library( + name = "polynomial_subject", + testonly = True, + srcs = [ + "PolynomialSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":real_subject", + "//model/src/main/proto:math_java_proto_lite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + ], +) + kt_android_library( name = "real_subject", testonly = True, diff --git a/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt new file mode 100644 index 00000000000..6b05db139a5 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt @@ -0,0 +1,79 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.StringSubject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.Polynomial +import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.oppia.android.util.math.getConstant +import org.oppia.android.util.math.isConstant +import org.oppia.android.util.math.toPlainText + +class PolynomialSubject( + metadata: FailureMetadata, + private val actual: Polynomial? +) : LiteProtoSubject(metadata, actual) { + private val nonNullActual by lazy { + checkNotNull(actual) { + "Expected polynomial to be defined, not null (is the expression/equation not a valid" + + " polynomial?)" + } + } + + fun isNotValidPolynomial() { + // TODO: use toPlainText here. + assertWithMessage( + "Expected polynomial to be undefined, but was: ${actual?.toPlainText()}" + ).that(actual).isNull() + } + + fun isConstantThat(): RealSubject { + // TODO: use toPlainText here. + assertWithMessage("Expected polynomial to be constant, but was: $nonNullActual") + .that(nonNullActual.isConstant()) + .isTrue() + return assertThat(nonNullActual.getConstant()) + } + + fun hasTermCountThat(): IntegerSubject = assertThat(nonNullActual.termCount) + + fun term(index: Int): PolynomialTermSubject = assertThat(nonNullActual.termList[index]) + + fun evaluatesToPlainTextThat(): StringSubject = assertThat(nonNullActual.toPlainText()) + + companion object { + fun assertThat(actual: Polynomial?): PolynomialSubject = + assertAbout(::PolynomialSubject).that(actual) + + private fun assertThat(actual: Polynomial.Term): PolynomialTermSubject = + assertAbout(::PolynomialTermSubject).that(actual) + + private fun assertThat(actual: Polynomial.Term.Variable): PolynomialTermVariableSubject = + assertAbout(::PolynomialTermVariableSubject).that(actual) + } + + class PolynomialTermSubject( + metadata: FailureMetadata, + private val actual: Polynomial.Term + ) : LiteProtoSubject(metadata, actual) { + fun hasCoefficientThat(): RealSubject = assertThat(actual.coefficient) + + fun hasVariableCountThat(): IntegerSubject = assertThat(actual.variableCount) + + fun variable(index: Int): PolynomialTermVariableSubject = + assertThat(actual.variableList[index]) + } + + class PolynomialTermVariableSubject( + metadata: FailureMetadata, + private val actual: Polynomial.Term.Variable + ) : LiteProtoSubject(metadata, actual) { + fun hasNameThat(): StringSubject = assertThat(actual.name) + + fun hasPowerThat(): IntegerSubject = assertThat(actual.power) + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 4b84961d297..054b7df78bb 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -9,7 +9,9 @@ kt_android_library( srcs = [ "FloatExtensions.kt", "FractionExtensions.kt", + "PolynomialExtensions.kt", "RatioExtensions.kt", + "RealExtensions.kt", ], visibility = [ "//:oppia_api_visibility", diff --git a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt index 62504046c78..2ca5ece9da3 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt @@ -14,3 +14,5 @@ fun Float.approximatelyEquals(other: Float): Boolean { fun Double.approximatelyEquals(other: Double): Boolean { return abs(this - other) < FLOAT_EQUALITY_INTERVAL } + +fun Double.toPlainString(): String = toBigDecimal().toPlainString() diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index d4a57faf9be..1da9fef1857 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -2,6 +2,19 @@ package org.oppia.android.util.math import org.oppia.android.app.model.Fraction +/** Returns whether this fraction has a fractional component. */ +fun Fraction.hasFractionalPart(): Boolean { + return numerator != 0 +} + +/** + * Returns whether this fraction only represents a whole number. Note that for the fraction '0' this + * will return true. + */ +fun Fraction.isOnlyWholeNumber(): Boolean { + return !hasFractionalPart() +} + /** * Returns a [Double] version of this fraction. * @@ -13,6 +26,35 @@ fun Fraction.toDouble(): Double { return if (isNegative) -doubleVal else doubleVal } +/** + * Returns a submittable answer string representation of this fraction (note that this may not be + * the verbatim string originally submitted by the user, if any. + */ +fun Fraction.toAnswerString(): String { + return when { + isOnlyWholeNumber() -> { + // Fraction is only a whole number. + if (isNegative) "-$wholeNumber" else "$wholeNumber" + } + wholeNumber == 0 -> { + // Fraction contains just a fraction (no whole number). + when (denominator) { + 1 -> if (isNegative) "-$numerator" else "$numerator" + else -> if (isNegative) "-$numerator/$denominator" else "$numerator/$denominator" + } + } + else -> { + // Otherwise it's a mixed number. Note that the denominator is always shown here to account + // for strange cases that would require evaluation to resolve, such as: "2 2/1". + if (isNegative) { + "-$wholeNumber $numerator/$denominator" + } else { + "$wholeNumber $numerator/$denominator" + } + } + } +} + /** * Returns this fraction in its most simplified form. * @@ -26,6 +68,23 @@ fun Fraction.toSimplestForm(): Fraction { }.build() } +/** + * Returns this fraction in an improper form (that is, with a 0 whole number and only fractional + * parts). + */ +fun Fraction.toImproperForm(): Fraction { + val newNumerator = numerator + (denominator * wholeNumber) + return toBuilder().apply { + numerator = newNumerator + wholeNumber = 0 + }.build() +} + +/** Returns the negated form of this fraction. */ +operator fun Fraction.unaryMinus(): Fraction { + return toBuilder().apply { isNegative = !this@unaryMinus.isNegative }.build() +} + /** Returns the greatest common divisor between two integers. */ fun gcd(x: Int, y: Int): Int { return if (y == 0) x else gcd(y, x % y) diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt new file mode 100644 index 00000000000..e1b98934566 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -0,0 +1,56 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.Polynomial +import org.oppia.android.app.model.Polynomial.Term +import org.oppia.android.app.model.Polynomial.Term.Variable +import org.oppia.android.app.model.Real +import org.oppia.android.app.model.Real.RealTypeCase.INTEGER +import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET + +/** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ +fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 + +/** + * Returns the first term coefficient from this polynomial. This corresponds to the whole value of + * the polynomial iff isConstant() returns true, otherwise this value isn't useful. + * + * Note that this function can throw if the polynomial is empty (so isConstant() should always be + * checked first). + */ +fun Polynomial.getConstant(): Real = getTerm(0).coefficient + +fun Polynomial.toPlainText(): String { + return termList.map { it.toPlainText() }.reduce { acc, termAnswerStr -> + if (termAnswerStr.startsWith("-")) { + "$acc - ${termAnswerStr.drop(1)}" + } else "$acc + $termAnswerStr" + } +} + +private fun Term.toPlainText(): String { + val productValues = mutableListOf() + + // Include the coefficient if there is one (coefficients of 1 are ignored only if there are + // variables present). + productValues += when { + variableList.isEmpty() || !abs(coefficient).isApproximatelyEqualTo(1.0) -> when { + coefficient.isRational() && variableList.isNotEmpty() -> "(${coefficient.toPlainText()})" + else -> coefficient.toPlainText() + } + coefficient.isNegative() -> "-" + else -> "" + } + + // Include any present variables. + productValues += variableList.map(Variable::toPlainText) + + // Take the product of all relevant values of the term. + return productValues.joinToString(separator = "") +} + +private fun Variable.toPlainText(): String { + return if (power > 1) "$name^$power" else name +} + diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt new file mode 100644 index 00000000000..6df36abd3b6 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -0,0 +1,53 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.Real +import org.oppia.android.app.model.Real.RealTypeCase.INTEGER +import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET + +fun Real.isRational(): Boolean = realTypeCase == RATIONAL + +fun Real.isNegative(): Boolean = when (realTypeCase) { + RATIONAL -> rational.isNegative + IRRATIONAL -> irrational < 0 + INTEGER -> integer < 0 + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") +} + +fun Real.toDouble(): Double { + return when (realTypeCase) { + RATIONAL -> rational.toDouble() + INTEGER -> integer.toDouble() + IRRATIONAL -> irrational + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + } +} + +fun Real.toPlainText(): String = when (realTypeCase) { + // Note that the rational part is first converted to an improper fraction since mixed fractions + // can't be expressed as a single coefficient in typical polynomial syntax). + RATIONAL -> rational.toImproperForm().toAnswerString() + IRRATIONAL -> irrational.toPlainString() + INTEGER -> integer.toString() + REALTYPE_NOT_SET, null -> "" +} + +fun Real.isApproximatelyEqualTo(value: Double): Boolean { + return toDouble().approximatelyEquals(value) +} + +operator fun Real.unaryMinus(): Real { + return when (realTypeCase) { + RATIONAL -> recompute { it.setRational(-rational) } + IRRATIONAL -> recompute { it.setIrrational(-irrational) } + INTEGER -> recompute { it.setInteger(-integer) } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + } +} + +fun abs(real: Real): Real = if (real.isNegative()) -real else real + +private fun Real.recompute(transform: (Real.Builder) -> Real.Builder): Real { + return transform(newBuilderForType()).build() +} From 9ffa70203e9dc6a15ee7c7b7262789e20acef2be Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 20:01:41 -0800 Subject: [PATCH 104/289] Migrate tests to shared test utilities. This applies to polynomials. --- .../util/math/MathExpressionExtensions.kt | 89 ------------------- .../org/oppia/android/util/math/BUILD.bazel | 1 + .../util/math/MathExpressionParserTest.kt | 73 +-------------- 3 files changed, 2 insertions(+), 161 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 3708f392ae6..f1386a1733e 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -713,48 +713,6 @@ private fun Polynomial.sort() = Polynomial.newBuilder().apply { addAllTerm(this@sort.termList.sortedWith(POLYNOMIAL_TERM_COMPARATOR)) }.build() -fun Polynomial.toPlainText(): String { - return termList.map { it.toPlainText() }.reduce { acc, termAnswerStr -> - if (termAnswerStr.startsWith("-")) { - "$acc - ${termAnswerStr.drop(1)}" - } else "$acc + $termAnswerStr" - } -} - -private fun Term.toPlainText(): String { - val productValues = mutableListOf() - - // Include the coefficient if there is one (coefficients of 1 are ignored only if there are - // variables present). - productValues += when { - variableList.isEmpty() || !abs(coefficient).isApproximatelyEqualTo(1.0) -> when { - coefficient.isRational() && variableList.isNotEmpty() -> "(${coefficient.toPlainText()})" - else -> coefficient.toPlainText() - } - coefficient.isNegative() -> "-" - else -> "" - } - - // Include any present variables. - productValues += variableList.map(Variable::toPlainText) - - // Take the product of all relevant values of the term. - return productValues.joinToString(separator = "") -} - -private fun Variable.toPlainText(): String { - return if (power > 1) "$name^$power" else name -} - -private fun Real.toPlainText(): String = when (realTypeCase) { - // Note that the rational part is first converted to an improper fraction since mixed fractions - // can't be expressed as a single coefficient in typical polynomial syntax). - RATIONAL -> rational.toImproperForm().toAnswerString() - IRRATIONAL -> irrational.toPlainString() - INTEGER -> integer.toString() - REALTYPE_NOT_SET, null -> "" -} - private fun Real.toPlainString(): String = when (realTypeCase) { RATIONAL -> rational.toDouble().toPlainString() IRRATIONAL -> irrational.toPlainString() @@ -794,18 +752,6 @@ private fun MathBinaryOperation.reduceToPolynomial(): Polynomial? { } } -/** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ -fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 - -/** - * Returns the first term coefficient from this polynomial. This corresponds to the whole value of - * the polynomial iff isConstant() returns true, otherwise this value isn't useful. - * - * Note that this function can throw if the polynomial is empty (so isConstant() should always be - * checked first). - */ -fun Polynomial.getConstant(): Real = getTerm(0).coefficient - private operator fun Polynomial.unaryMinus(): Polynomial { // Negating a polynomial just requires flipping the signs on all coefficients. return toBuilder() @@ -1106,21 +1052,8 @@ private fun createZeroTerm() = Term.newBuilder().apply { coefficient = createCoefficientValueOfZero() }.build() -private fun Real.isApproximatelyEqualTo(value: Double): Boolean { - return toDouble().approximatelyEquals(value) -} - private fun Real.isApproximatelyZero(): Boolean = isApproximatelyEqualTo(0.0) -fun Real.toDouble(): Double { - return when (realTypeCase) { - RATIONAL -> rational.toDouble() - INTEGER -> integer.toDouble() - IRRATIONAL -> irrational - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") - } -} - private fun Real.recompute(transform: (Real.Builder) -> Real.Builder): Real { return transform(toBuilder().clearRational().clearIrrational().clearInteger()).build() } @@ -1239,15 +1172,6 @@ private fun Real.pow(rhs: Real): Real { } } -private operator fun Real.unaryMinus(): Real { - return when (realTypeCase) { - RATIONAL -> recompute { it.setRational(-rational) } - IRRATIONAL -> recompute { it.setIrrational(-irrational) } - INTEGER -> recompute { it.setInteger(-integer) } - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") - } -} - private operator fun Real.plus(rhs: Real): Real { return combine( this, rhs, Fraction::plus, Fraction::plus, Fraction::plus, Double::plus, Double::plus, @@ -1285,17 +1209,8 @@ private fun sqrt(real: Real): Real { } } -private fun abs(real: Real): Real = if (real.isNegative()) -real else real - private fun Real.isInteger(): Boolean = realTypeCase == INTEGER -private fun Real.isNegative(): Boolean = when (realTypeCase) { - RATIONAL -> rational.isNegative - IRRATIONAL -> irrational < 0 - INTEGER -> integer < 0 - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") -} - private fun Real.asWholeNumber(): Int? { return when (realTypeCase) { RATIONAL -> if (rational.isOnlyWholeNumber()) rational.toWholeNumber() else null @@ -1313,8 +1228,6 @@ private fun Real.isWholeNumber(): Boolean { } } -private fun Real.isRational(): Boolean = realTypeCase == RATIONAL - private fun Double.pow(rhs: Fraction): Double = this.pow(rhs.toDouble()) private fun Fraction.pow(rhs: Double): Double = toDouble().pow(rhs) private operator fun Double.plus(rhs: Fraction): Double = this + rhs.toFloat() @@ -1431,5 +1344,3 @@ private fun sqrt(int: Int): Real { irrational = sqrt(int.toDouble()) }.build() } - -private fun Double.toPlainString(): String = toBigDecimal().toPlainString() diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 8f1e4145718..4433b423dc7 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -16,6 +16,7 @@ oppia_android_test( "//testing/src/main/java/org/oppia/android/testing/math:fraction_subject", "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", + "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", "//testing/src/main/java/org/oppia/android/testing/math:real_subject", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_extensions_truth-liteproto-extension", diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index 594c4f718cf..5862513378a 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -1,23 +1,16 @@ package org.oppia.android.util.math import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.BooleanSubject import com.google.common.truth.DoubleSubject -import com.google.common.truth.FailureMetadata import com.google.common.truth.IntegerSubject import com.google.common.truth.StringSubject -import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage -import com.google.common.truth.extensions.proto.LiteProtoSubject import kotlin.math.sqrt import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.ComparableOperationList import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.PRODUCT import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase import org.oppia.android.app.model.MathBinaryOperation import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression @@ -31,9 +24,7 @@ import org.oppia.android.app.model.OppiaLanguage.HINGLISH import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED -import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Real -import org.oppia.android.testing.math.ComparableOperationListSubject import org.oppia.android.testing.math.ComparableOperationListSubject.Companion.assertThat import org.oppia.android.testing.math.FractionSubject import org.oppia.android.testing.math.FractionSubject.Companion.assertThat @@ -41,8 +32,7 @@ import org.oppia.android.testing.math.MathEquationSubject import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat import org.oppia.android.testing.math.MathExpressionSubject import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat -import org.oppia.android.testing.math.RealSubject -import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError @@ -7203,58 +7193,6 @@ class MathExpressionParserTest { } } - private class PolynomialSubject( - metadata: FailureMetadata, - private val actual: Polynomial? - ) : LiteProtoSubject(metadata, actual) { - private val nonNullActual by lazy { - checkNotNull(actual) { - "Expected polynomial to be defined, not null (is the expression/equation not a valid" + - " polynomial?)" - } - } - - fun isNotValidPolynomial() { - assertWithMessage( - "Expected polynomial to be undefined, but was: ${actual?.toPlainText()}" - ).that(actual).isNull() - } - - fun isConstantThat(): RealSubject { - assertWithMessage("Expected polynomial to be constant: $nonNullActual") - .that(nonNullActual.isConstant()) - .isTrue() - return assertThat(nonNullActual.getConstant()) - } - - fun hasTermCountThat(): IntegerSubject = assertThat(nonNullActual.termCount) - - fun term(index: Int): PolynomialTermSubject = assertThat(nonNullActual.termList[index]) - - fun evaluatesToPlainTextThat(): StringSubject = assertThat(nonNullActual.toPlainText()) - } - - private class PolynomialTermSubject( - metadata: FailureMetadata, - private val actual: Polynomial.Term - ) : LiteProtoSubject(metadata, actual) { - fun hasCoefficientThat(): RealSubject = assertThat(actual.coefficient) - - fun hasVariableCountThat(): IntegerSubject = assertThat(actual.variableCount) - - fun variable(index: Int): PolynomialTermVariableSubject = - assertThat(actual.variableList[index]) - } - - private class PolynomialTermVariableSubject( - metadata: FailureMetadata, - private val actual: Polynomial.Term.Variable - ) : LiteProtoSubject(metadata, actual) { - fun hasNameThat(): StringSubject = assertThat(actual.name) - - fun hasPowerThat(): IntegerSubject = assertThat(actual.power) - } - private companion object { // TODO: fix helper API. @@ -7355,14 +7293,5 @@ class MathExpressionParserTest { expression, allowedVariables, errorCheckingMode ) } - - private fun assertThat(actual: Polynomial?): PolynomialSubject = - assertAbout(::PolynomialSubject).that(actual) - - private fun assertThat(actual: Polynomial.Term): PolynomialTermSubject = - assertAbout(::PolynomialTermSubject).that(actual) - - private fun assertThat(actual: Polynomial.Term.Variable): PolynomialTermVariableSubject = - assertAbout(::PolynomialTermVariableSubject).that(actual) } } From 7c36fdfbe93e4f13319344a4dc118825c244304a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 23:15:30 -0800 Subject: [PATCH 105/289] Lint fix. --- ...utIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt index af698227520..f9498f7d965 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt @@ -36,7 +36,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider input: Fraction, writtenTranslationContext: WrittenTranslationContext ): Boolean { - return answer.toDouble().approximatelyEquals(input.toDouble()) - && answer == input.toSimplestForm() + return answer.toDouble().approximatelyEquals(input.toDouble()) && + answer == input.toSimplestForm() } } From d430f8c826054b6d9c4a7d1fc5b7da9f4297cc1f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 23:17:24 -0800 Subject: [PATCH 106/289] Lint fixes. --- .../oppia/android/domain/util/InteractionObjectExtensions.kt | 1 - .../java/org/oppia/android/testing/math/PolynomialSubject.kt | 2 +- .../java/org/oppia/android/util/math/PolynomialExtensions.kt | 5 ----- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt b/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt index 11d3af018a9..32f9123e852 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt +++ b/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt @@ -1,7 +1,6 @@ package org.oppia.android.domain.util import org.oppia.android.app.model.ClickOnImage -import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.ImageWithRegions import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.BOOL_VALUE diff --git a/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt index 6b05db139a5..bb4a3d970d5 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt @@ -44,7 +44,7 @@ class PolynomialSubject( fun term(index: Int): PolynomialTermSubject = assertThat(nonNullActual.termList[index]) fun evaluatesToPlainTextThat(): StringSubject = assertThat(nonNullActual.toPlainText()) - + companion object { fun assertThat(actual: Polynomial?): PolynomialSubject = assertAbout(::PolynomialSubject).that(actual) diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index e1b98934566..a4ba72213be 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -4,10 +4,6 @@ import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Polynomial.Term import org.oppia.android.app.model.Polynomial.Term.Variable import org.oppia.android.app.model.Real -import org.oppia.android.app.model.Real.RealTypeCase.INTEGER -import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL -import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL -import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET /** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 @@ -53,4 +49,3 @@ private fun Term.toPlainText(): String { private fun Variable.toPlainText(): String { return if (power > 1) "$name^$power" else name } - From 0099d67a87ea22f7ad26fb3536453646127dc077 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 23:34:12 -0800 Subject: [PATCH 107/289] Add math tokenizer + utility & tests. This is mostly copied from #2173. --- .../org/oppia/android/util/math/BUILD.bazel | 21 + .../oppia/android/util/math/MathTokenizer.kt | 381 ++++++++++++++++++ .../android/util/math/PeekableIterator.kt | 38 ++ .../org/oppia/android/util/math/BUILD.bazel | 18 + .../android/util/math/MathTokenizerTest.kt | 195 +++++++++ 5 files changed, 653 insertions(+) create mode 100644 utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 054b7df78bb..1c099b59d0f 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -20,3 +20,24 @@ kt_android_library( "//model/src/main/proto:math_java_proto_lite", ], ) + +kt_android_library( + name = "tokenizer", + srcs = [ + "MathTokenizer.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":peekable_iterator", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "peekable_iterator", + srcs = [ + "PeekableIterator.kt", + ], +) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt new file mode 100644 index 00000000000..37ca0410cd0 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt @@ -0,0 +1,381 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import java.lang.StringBuilder + +// TODO: rename to MathTokenizer & add documentation. +// TODO: consider a more efficient implementation that uses 1 underlying buffer (which could still +// be sequence-backed) with a forced lookahead-of-1 API, to also avoid rebuffering when parsing +// sequences of characters like for integers. + +// TODO: add customization to omit certain symbols (such as variables for numeric expressions?) +class MathTokenizer private constructor() { + companion object { + fun tokenize(input: String): Sequence = tokenize(input.toCharArray().asSequence()) + + fun tokenize(input: Sequence): Sequence { + val chars = PeekableIterator.fromSequence(input) + return generateSequence { + // Consume any whitespace that might precede a valid token. + chars.consumeWhitespace() + + // Parse the next token from the underlying sequence. + when (chars.peek()) { + in '0'..'9' -> tokenizeIntegerOrRealNumber(chars) + in 'a'..'z', in 'A'..'Z' -> tokenizeVariableOrFunctionName(chars) + '√' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.SquareRootSymbol(startIndex, endIndex) + } + '+' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.PlusSymbol(startIndex, endIndex) + } + // TODO: add tests for different subtraction/minus symbols. + '-', '−' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.MinusSymbol(startIndex, endIndex) + } + '*', '×' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.MultiplySymbol(startIndex, endIndex) + } + '/', '÷' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.DivideSymbol(startIndex, endIndex) + } + '^' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.ExponentiationSymbol(startIndex, endIndex) + } + '=' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.EqualsSymbol(startIndex, endIndex) + } + '(' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.LeftParenthesisSymbol(startIndex, endIndex) + } + ')' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.RightParenthesisSymbol(startIndex, endIndex) + } + null -> null // End of stream. + // Invalid character. + else -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.InvalidToken(startIndex, endIndex) + } + } + } + } + + private fun tokenizeIntegerOrRealNumber(chars: PeekableIterator): Token { + val startIndex = chars.getRetrievalCount() + val integerPart1 = + parseInteger(chars) + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) + chars.consumeWhitespace() // Whitespace is allowed between digits and the '.'. + return if (chars.peek() == '.') { + chars.next() // Parse the "." since it will be re-added later. + chars.consumeWhitespace() // Whitespace is allowed between the '.' and following digits. + + // Another integer must follow the ".". + val integerPart2 = parseInteger(chars) + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) + + // TODO: validate that the result isn't NaN or INF. + val doubleValue = "$integerPart1.$integerPart2".toDoubleOrNull() + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) + Token.PositiveRealNumber(doubleValue, startIndex, endIndex = chars.getRetrievalCount()) + } else { + Token.PositiveInteger( + integerPart1.toIntOrNull() + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()), + startIndex, + endIndex = chars.getRetrievalCount() + ) + } + } + + private fun tokenizeVariableOrFunctionName(chars: PeekableIterator): Token { + val startIndex = chars.getRetrievalCount() + val firstChar = chars.next() + + // latin_letter = lowercase_latin_letter | uppercase_latin_letter ; + // variable = latin_letter ; + return tokenizeFunctionName(firstChar, startIndex, chars) + ?: Token.VariableName( + firstChar.toString(), startIndex, endIndex = chars.getRetrievalCount() + ) + } + + private fun tokenizeFunctionName( + currChar: Char, + startIndex: Int, + chars: PeekableIterator + ): Token? { + // allowed_function_name = "sqrt" ; + // disallowed_function_name = + // "exp" | "log" | "log10" | "ln" | "sin" | "cos" | "tan" | "cot" | "csc" + // | "sec" | "atan" | "asin" | "acos" | "abs" ; + // function_name = allowed_function_name | disallowed_function_name ; + val nextChar = chars.peek() + return when (currChar) { + 'a' -> { + // abs, acos, asin, atan, or variable. + when (nextChar) { + 'b' -> + tokenizeExpectedFunction(name = "abs", isAllowedFunction = false, startIndex, chars) + 'c' -> + tokenizeExpectedFunction(name = "acos", isAllowedFunction = false, startIndex, chars) + 's' -> + tokenizeExpectedFunction(name = "asin", isAllowedFunction = false, startIndex, chars) + 't' -> + tokenizeExpectedFunction(name = "atan", isAllowedFunction = false, startIndex, chars) + else -> null // Must be a variable. + } + } + 'c' -> { + // cos, cot, csc, or variable. + when (nextChar) { + 'o' -> { + chars.next() // Skip the 'o' to go to the last character. + val name = if (chars.peek() == 's') { + chars.expectNextMatches { it == 's' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + "cos" + } else { + // Otherwise, it must be 'c' for 'cot' since the parser can't backtrack. + chars.expectNextMatches { it == 't' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + "cot" + } + Token.FunctionName( + name, isAllowedFunction = false, startIndex, endIndex = chars.getRetrievalCount() + ) + } + 's' -> + tokenizeExpectedFunction(name = "csc", isAllowedFunction = false, startIndex, chars) + else -> null // Must be a variable. + } + } + 'e' -> { + // exp or variable. + if (nextChar == 'x') { + tokenizeExpectedFunction(name = "exp", isAllowedFunction = false, startIndex, chars) + } else null // Must be a variable. + } + 'l' -> { + // ln, log, log10, or variable. + when (nextChar) { + 'n' -> + tokenizeExpectedFunction(name = "ln", isAllowedFunction = false, startIndex, chars) + 'o' -> { + // Skip the 'o'. Following the 'o' must be a 'g' since the parser can't backtrack. + chars.next() + chars.expectNextMatches { it == 'g' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + val name = if (chars.peek() == '1') { + // '10' must be next for 'log10'. + chars.expectNextMatches { it == '1' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + chars.expectNextMatches { it == '0' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + "log10" + } else "log" + Token.FunctionName( + name, isAllowedFunction = false, startIndex, endIndex = chars.getRetrievalCount() + ) + } + else -> null // Must be a variable. + } + } + 's' -> { + // sec, sin, sqrt, or variable. + when (nextChar) { + 'e' -> + tokenizeExpectedFunction(name = "sec", isAllowedFunction = false, startIndex, chars) + 'i' -> + tokenizeExpectedFunction(name = "sin", isAllowedFunction = false, startIndex, chars) + 'q' -> + tokenizeExpectedFunction(name = "sqrt", isAllowedFunction = true, startIndex, chars) + else -> null // Must be a variable. + } + } + 't' -> { + // tan or variable. + if (nextChar == 'a') { + tokenizeExpectedFunction(name = "tan", isAllowedFunction = false, startIndex, chars) + } else null // Must be a variable. + } + else -> null // Must be a variable since no known functions match the first character. + } + } + + private fun tokenizeExpectedFunction( + name: String, + isAllowedFunction: Boolean, + startIndex: Int, + chars: PeekableIterator + ): Token { + return chars.expectNextCharsForFunctionName(name.substring(1), startIndex) + ?: Token.FunctionName( + name, isAllowedFunction, startIndex, endIndex = chars.getRetrievalCount() + ) + } + + private fun tokenizeSymbol(chars: PeekableIterator, factory: (Int, Int) -> Token): Token { + val startIndex = chars.getRetrievalCount() + chars.next() // Parse the symbol. + val endIndex = chars.getRetrievalCount() + return factory(startIndex, endIndex) + } + + private fun parseInteger(chars: PeekableIterator): String? { + val integerBuilder = StringBuilder() + while (chars.peek() in '0'..'9') { + integerBuilder.append(chars.next()) + } + return if (integerBuilder.isNotEmpty()) { + integerBuilder.toString() + } else null // Failed to parse; no digits. + } + + interface UnaryOperatorToken { + fun getUnaryOperator(): MathUnaryOperation.Operator + } + + interface BinaryOperatorToken { + fun getBinaryOperator(): MathBinaryOperation.Operator + } + + sealed class Token { + /** The index in the input stream at which point this token begins. */ + abstract val startIndex: Int + + /** The (exclusive) index in the input stream at which point this token ends. */ + abstract val endIndex: Int + + class PositiveInteger( + val parsedValue: Int, + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class PositiveRealNumber( + val parsedValue: Double, + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class VariableName( + val parsedName: String, + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class FunctionName( + val parsedName: String, + val isAllowedFunction: Boolean, + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class MinusSymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token(), UnaryOperatorToken, BinaryOperatorToken { + override fun getUnaryOperator(): MathUnaryOperation.Operator = NEGATE + + override fun getBinaryOperator(): MathBinaryOperation.Operator = SUBTRACT + } + + class SquareRootSymbol(override val startIndex: Int, override val endIndex: Int) : Token() + + class PlusSymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token(), UnaryOperatorToken, BinaryOperatorToken { + override fun getUnaryOperator(): MathUnaryOperation.Operator = POSITIVE + + override fun getBinaryOperator(): MathBinaryOperation.Operator = ADD + } + + class MultiplySymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token(), BinaryOperatorToken { + override fun getBinaryOperator(): MathBinaryOperation.Operator = MULTIPLY + } + + class DivideSymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token(), BinaryOperatorToken { + override fun getBinaryOperator(): MathBinaryOperation.Operator = DIVIDE + } + + class ExponentiationSymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token(), BinaryOperatorToken { + override fun getBinaryOperator(): MathBinaryOperation.Operator = EXPONENTIATE + } + + class EqualsSymbol(override val startIndex: Int, override val endIndex: Int) : Token() + + class LeftParenthesisSymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class RightParenthesisSymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class IncompleteFunctionName( + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class InvalidToken(override val startIndex: Int, override val endIndex: Int) : Token() + } + + // TODO: consider whether to use the more correct & expensive Java Char.isWhitespace(). + private fun Char.isWhitespace(): Boolean = when (this) { + ' ', '\t', '\n', '\r' -> true + else -> false + } + + private fun PeekableIterator.consumeWhitespace() { + while (peek()?.isWhitespace() == true) next() + } + + /** + * Expects each of the characters to be next in the token stream, in the order of the string. + * All characters must be present in [this] iterator. Returns non-null if a failure occurs, + * otherwise null if all characters were confirmed to be present. If null is returned, [this] + * iterator will be at the token that comes after the last confirmed character in the string. + */ + private fun PeekableIterator.expectNextCharsForFunctionName( + chars: String, + startIndex: Int + ): Token? { + for (c in chars) { + expectNextValue { c } + ?: return Token.IncompleteFunctionName(startIndex, endIndex = getRetrievalCount()) + } + return null + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt b/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt new file mode 100644 index 00000000000..1a7abacc061 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt @@ -0,0 +1,38 @@ +package org.oppia.android.util.math + +class PeekableIterator(private val backingIterator: Iterator) : Iterator { + private var next: T? = null + private var count: Int = 0 + + override fun hasNext(): Boolean = next != null || backingIterator.hasNext() + + override fun next(): T = next?.also { + next = null + count++ + } ?: retrieveNext() + + fun peek(): T? { + return when { + next != null -> next + hasNext() -> retrieveNext().also { next = it } + else -> null + } + } + + fun expectNextValue(expected: () -> T): T? = expectNextMatches { it == expected() } + + fun expectNextMatches(predicate: (T) -> Boolean): T? { + // Only call the predicate if not at the end of the stream, and only call next() if the next + // value matches. + return peek()?.takeIf(predicate)?.also { next() } + } + + fun getRetrievalCount(): Int = count + + private fun retrieveNext(): T = backingIterator.next() + + companion object { + fun fromSequence(sequence: Sequence): PeekableIterator = + PeekableIterator(sequence.iterator()) + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 493d89d66a0..d918580ffb9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -4,6 +4,24 @@ TODO: document load("//:oppia_android_test.bzl", "oppia_android_test") +oppia_android_test( + name = "MathTokenizerTest", + srcs = ["MathTokenizerTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.MathTokenizerTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing:assertion_helpers", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:tokenizer", + ], +) + oppia_android_test( name = "RatioExtensionsTest", srcs = ["RatioExtensionsTest.kt"], diff --git a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt new file mode 100644 index 00000000000..ac7e6556b9c --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt @@ -0,0 +1,195 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.DoubleSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.StringSubject +import com.google.common.truth.Subject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.util.math.MathTokenizer.Companion.Token +import org.robolectric.annotation.LooperMode + +/** Tests for [MathTokenizer]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class MathTokenizerTest { + @Test + fun testLotsOfCases() { + // TODO: split this up + // testTokenize_emptyString_producesNoTokens + val tokens1 = MathTokenizer.tokenize(" ").toList() + assertThat(tokens1).isEmpty() + + val tokens2 = MathTokenizer.tokenize(" 2 ").toList() + assertThat(tokens2).hasSize(1) + assertThat(tokens2.first()).isPositiveIntegerWhoseValue().isEqualTo(2) + + val tokens3 = MathTokenizer.tokenize(" 2.5 ").toList() + assertThat(tokens3).hasSize(1) + assertThat(tokens3.first()).isPositiveRealNumberWhoseValue().isWithin(1e-5).of(2.5) + + val tokens4 = MathTokenizer.tokenize(" x ").toList() + assertThat(tokens4).hasSize(1) + assertThat(tokens4.first()).isVariableWhoseName().isEqualTo("x") + + val tokens5 = MathTokenizer.tokenize(" z x ").toList() + assertThat(tokens5).hasSize(2) + assertThat(tokens5[0]).isVariableWhoseName().isEqualTo("z") + assertThat(tokens5[1]).isVariableWhoseName().isEqualTo("x") + + val tokens6 = MathTokenizer.tokenize("2^3^2").toList() + assertThat(tokens6).hasSize(5) + assertThat(tokens6[0]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens6[1]).isExponentiationSymbol() + assertThat(tokens6[2]).isPositiveIntegerWhoseValue().isEqualTo(3) + assertThat(tokens6[3]).isExponentiationSymbol() + assertThat(tokens6[4]).isPositiveIntegerWhoseValue().isEqualTo(2) + + val tokens7 = MathTokenizer.tokenize("sqrt(2)").toList() + assertThat(tokens7).hasSize(4) + assertThat(tokens7[0]).isFunctionWhoseName().isEqualTo("sqrt") + assertThat(tokens7[1]).isLeftParenthesisSymbol() + assertThat(tokens7[2]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens7[3]).isRightParenthesisSymbol() + + val tokens8 = MathTokenizer.tokenize("sqr(2)").toList() + assertThat(tokens8).hasSize(4) + assertThat(tokens8[0]).isIncompleteFunctionName() + assertThat(tokens8[1]).isLeftParenthesisSymbol() + assertThat(tokens8[2]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens8[3]).isRightParenthesisSymbol() + + val tokens9 = MathTokenizer.tokenize("xyz(2)").toList() + assertThat(tokens9).hasSize(6) + assertThat(tokens9[0]).isVariableWhoseName().isEqualTo("x") + assertThat(tokens9[1]).isVariableWhoseName().isEqualTo("y") + assertThat(tokens9[2]).isVariableWhoseName().isEqualTo("z") + assertThat(tokens9[3]).isLeftParenthesisSymbol() + assertThat(tokens9[4]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens9[5]).isRightParenthesisSymbol() + + val tokens10 = MathTokenizer.tokenize("732").toList() + assertThat(tokens10).hasSize(1) + assertThat(tokens10.first()).isPositiveIntegerWhoseValue().isEqualTo(732) + + val tokens11 = MathTokenizer.tokenize("73 2").toList() + assertThat(tokens11).hasSize(2) + assertThat(tokens11[0]).isPositiveIntegerWhoseValue().isEqualTo(73) + assertThat(tokens11[1]).isPositiveIntegerWhoseValue().isEqualTo(2) + + val tokens12 = MathTokenizer.tokenize("1*2-3+4^7-8/3*2+7").toList() + assertThat(tokens12).hasSize(17) + assertThat(tokens12[0]).isPositiveIntegerWhoseValue().isEqualTo(1) + assertThat(tokens12[1]).isMultiplySymbol() + assertThat(tokens12[2]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens12[3]).isMinusSymbol() + assertThat(tokens12[4]).isPositiveIntegerWhoseValue().isEqualTo(3) + assertThat(tokens12[5]).isPlusSymbol() + assertThat(tokens12[6]).isPositiveIntegerWhoseValue().isEqualTo(4) + assertThat(tokens12[7]).isExponentiationSymbol() + assertThat(tokens12[8]).isPositiveIntegerWhoseValue().isEqualTo(7) + assertThat(tokens12[9]).isMinusSymbol() + assertThat(tokens12[10]).isPositiveIntegerWhoseValue().isEqualTo(8) + assertThat(tokens12[11]).isDivideSymbol() + assertThat(tokens12[12]).isPositiveIntegerWhoseValue().isEqualTo(3) + assertThat(tokens12[13]).isMultiplySymbol() + assertThat(tokens12[14]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens12[15]).isPlusSymbol() + assertThat(tokens12[16]).isPositiveIntegerWhoseValue().isEqualTo(7) + + val tokens13 = MathTokenizer.tokenize("x = √2 × 7 ÷ 4").toList() + assertThat(tokens13).hasSize(8) + assertThat(tokens13[0]).isVariableWhoseName().isEqualTo("x") + assertThat(tokens13[1]).isEqualsSymbol() + assertThat(tokens13[2]).isSquareRootSymbol() + assertThat(tokens13[3]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens13[4]).isMultiplySymbol() + assertThat(tokens13[5]).isPositiveIntegerWhoseValue().isEqualTo(7) + assertThat(tokens13[6]).isDivideSymbol() + assertThat(tokens13[7]).isPositiveIntegerWhoseValue().isEqualTo(4) + } + + private class TokenSubject( + metadata: FailureMetadata, + private val actual: T + ) : Subject(metadata, actual) { + fun isPositiveIntegerWhoseValue(): IntegerSubject { + return assertThat(actual.asVerifiedType().parsedValue) + } + + fun isPositiveRealNumberWhoseValue(): DoubleSubject { + return assertThat(actual.asVerifiedType().parsedValue) + } + + fun isVariableWhoseName(): StringSubject { + return assertThat(actual.asVerifiedType().parsedName) + } + + fun isFunctionWhoseName(): StringSubject { + return assertThat(actual.asVerifiedType().parsedName) + } + + fun isMinusSymbol() { + actual.asVerifiedType() + } + + fun isSquareRootSymbol() { + actual.asVerifiedType() + } + + fun isPlusSymbol() { + actual.asVerifiedType() + } + + fun isMultiplySymbol() { + actual.asVerifiedType() + } + + fun isDivideSymbol() { + actual.asVerifiedType() + } + + fun isExponentiationSymbol() { + actual.asVerifiedType() + } + + fun isEqualsSymbol() { + actual.asVerifiedType() + } + + fun isLeftParenthesisSymbol() { + actual.asVerifiedType() + } + + fun isRightParenthesisSymbol() { + actual.asVerifiedType() + } + + fun isInvalidToken() { + actual.asVerifiedType() + } + + fun isIncompleteFunctionName() { + actual.asVerifiedType() + } + + private companion object { + private inline fun Token.asVerifiedType(): T { + assertThat(this).isInstanceOf(T::class.java) + return this as T + } + } + } + + private companion object { + private fun assertThat(actual: T): TokenSubject = + assertAbout(createTokenSubjectFactory()).that(actual) + + private fun createTokenSubjectFactory() = + Subject.Factory, T>(::TokenSubject) + } +} From 1d721d563071b57a4cf801dfec97f77da7663220 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 00:25:11 -0800 Subject: [PATCH 108/289] Add math expression/equation parsing support. This includes full error detection, and specific test suites for each parsing case. Much revisement is needed in the tests, and some additional issues may yet need to be fixed in the parser and/or error-detection logic. This is copied from #2173 with revisement & reduction since it's part of a multi-PR split. --- .../org/oppia/android/util/math/BUILD.bazel | 30 + .../android/util/math/MathExpressionParser.kt | 1044 +++++++++ .../android/util/math/MathParsingError.kt | 69 + .../util/math/AlgebraicEquationParserTest.kt | 227 ++ .../math/AlgebraicExpressionParserTest.kt | 1939 +++++++++++++++++ .../org/oppia/android/util/math/BUILD.bazel | 82 + .../util/math/MathExpressionParserTest.kt | 344 +++ .../util/math/NumericExpressionParserTest.kt | 1777 +++++++++++++++ 8 files changed, 5512 insertions(+) create mode 100644 utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 1c099b59d0f..1e0da7381b5 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -21,6 +21,36 @@ kt_android_library( ], ) +kt_android_library( + name = "parsing_error", + srcs = [ + "MathParsingError.kt", + ], + visibility = [ + "//:oppia_api_visibility", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "parser", + srcs = [ + "MathExpressionParser.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":extensions", + ":parsing_error", + ":peekable_iterator", + ":tokenizer", + "//model/src/main/proto:math_java_proto_lite", + ], +) + kt_android_library( name = "tokenizer", srcs = [ diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt new file mode 100644 index 00000000000..80b0acd49b3 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -0,0 +1,1044 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.Real +import org.oppia.android.util.math.MathExpressionParser.ParseContext.AlgebraicExpressionContext +import org.oppia.android.util.math.MathExpressionParser.ParseContext.NumericExpressionContext +import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError +import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError +import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError +import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError +import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError +import org.oppia.android.util.math.MathParsingError.FunctionNameIncompleteError +import org.oppia.android.util.math.MathParsingError.GenericError +import org.oppia.android.util.math.MathParsingError.HangingSquareRootError +import org.oppia.android.util.math.MathParsingError.InvalidFunctionInUseError +import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.NestedExponentsError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberAfterBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberBeforeBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NumberAfterVariableError +import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError +import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError +import org.oppia.android.util.math.MathParsingError.SubsequentBinaryOperatorsError +import org.oppia.android.util.math.MathParsingError.SubsequentUnaryOperatorsError +import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError +import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError +import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError +import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError +import org.oppia.android.util.math.MathTokenizer.Companion.BinaryOperatorToken +import org.oppia.android.util.math.MathTokenizer.Companion.Token +import org.oppia.android.util.math.MathTokenizer.Companion.Token.DivideSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.EqualsSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.ExponentiationSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.FunctionName +import org.oppia.android.util.math.MathTokenizer.Companion.Token.IncompleteFunctionName +import org.oppia.android.util.math.MathTokenizer.Companion.Token.InvalidToken +import org.oppia.android.util.math.MathTokenizer.Companion.Token.LeftParenthesisSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.MinusSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.MultiplySymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.PlusSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.PositiveInteger +import org.oppia.android.util.math.MathTokenizer.Companion.Token.PositiveRealNumber +import org.oppia.android.util.math.MathTokenizer.Companion.Token.RightParenthesisSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.SquareRootSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.VariableName +import kotlin.math.absoluteValue + +class MathExpressionParser private constructor(private val parseContext: ParseContext) { + // TODO: + // - Add helpers to reduce overall parser length. + // - Integrate with new errors & update the routines to not rely on exceptions except in actual exceptional cases. Make sure optional errors can be disabled (for testing purposes). + // - Rename this to be a generic parser, update the public API, add documentation, remove the old classes, and split up the big test routines into actual separate tests. + + // TODO: implement specific errors. + // TODO: verify remaining GenericErrors are correct. + + // TODO: document that 'generic' means either 'numeric' or 'algebraic' (ie that the expression is syntactically the same between both grammars). + // TODO: document that one design goal is keeping the grammar for this parser as LL(1) & why. + + private fun parseGenericEquationGrammar(): MathParsingResult { + // generic_equation_grammar = generic_equation ; + return parseGenericEquation().maybeFail { equation -> + checkForLearnerErrors(equation.leftSide) ?: checkForLearnerErrors(equation.rightSide) + } + } + + private fun parseGenericExpressionGrammar(): MathParsingResult { + // generic_expression_grammar = generic_expression ; + return parseGenericExpression().maybeFail { expression -> checkForLearnerErrors(expression) } + } + + private fun parseGenericEquation(): MathParsingResult { + // algebraic_equation = generic_expression , equals_operator , generic_expression ; + + if (parseContext.hasNextTokenOfType()) { + // If equals starts the string, then there's no LHS. + return EquationMissingLhsOrRhsError.toFailure() + } + + val lhsResult = parseGenericExpression().also { + parseContext.consumeTokenOfType() + }.maybeFail { + if (!parseContext.hasMoreTokens()) { + // If there are no tokens following the equals symbol, then there's no RHS. + EquationMissingLhsOrRhsError + } else null + } + + val rhsResult = lhsResult.flatMap { parseGenericExpression() } + return lhsResult.combineWith(rhsResult) { lhs, rhs -> + MathEquation.newBuilder().apply { + leftSide = lhs + rightSide = rhs + }.build() + } + } + + private fun parseGenericExpression(): MathParsingResult { + // generic_expression = generic_add_sub_expression ; + return parseGenericAddSubExpression() + } + + private fun parseGenericAddSubExpression(): MathParsingResult { + // generic_add_sub_expression = + // generic_mult_div_expression , { generic_add_sub_expression_rhs } ; + return parseGenericBinaryExpression( + parseLhs = this::parseGenericMultDivExpression + ) { nextToken -> + // generic_add_sub_expression_rhs = + // generic_add_expression_rhs | generic_sub_expression_rhs ; + when (nextToken) { + is PlusSymbol -> BinaryOperationRhs( + operator = ADD, + rhsResult = parseGenericAddExpressionRhs() + ) + is MinusSymbol -> BinaryOperationRhs( + operator = SUBTRACT, + rhsResult = parseGenericSubExpressionRhs() + ) + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is FunctionName, is InvalidToken, is LeftParenthesisSymbol, + is MultiplySymbol, is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, + is IncompleteFunctionName, null -> null + } + } + } + + private fun parseGenericAddExpressionRhs(): MathParsingResult { + // generic_add_expression_rhs = plus_operator , generic_mult_div_expression ; + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(ADD) + } else null + }.flatMap { + parseGenericMultDivExpression() + } + } + + private fun parseGenericSubExpressionRhs(): MathParsingResult { + // generic_sub_expression_rhs = minus_operator , generic_mult_div_expression ; + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(SUBTRACT) + } else null + }.flatMap { + parseGenericMultDivExpression() + } + } + + private fun parseGenericMultDivExpression(): MathParsingResult { + // generic_mult_div_expression = + // generic_exp_expression , { generic_mult_div_expression_rhs } ; + return parseGenericBinaryExpression( + parseLhs = this::parseGenericExpExpression + ) { nextToken -> + // generic_mult_div_expression_rhs = + // generic_mult_expression_rhs + // | generic_div_expression_rhs + // | generic_implicit_mult_expression_rhs ; + when (nextToken) { + is MultiplySymbol -> BinaryOperationRhs( + operator = MULTIPLY, + rhsResult = parseGenericMultExpressionRhs() + ) + is DivideSymbol -> BinaryOperationRhs( + operator = DIVIDE, + rhsResult = parseGenericDivExpressionRhs() + ) + is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> BinaryOperationRhs( + operator = MULTIPLY, + rhsResult = parseGenericImplicitMultExpressionRhs(), + isImplicit = true + ) + is VariableName -> { + if (parseContext is AlgebraicExpressionContext) { + BinaryOperationRhs( + operator = MULTIPLY, + rhsResult = parseGenericImplicitMultExpressionRhs(), + isImplicit = true + ) + } else null + } + // Not a match to the expression. + is PositiveInteger, is PositiveRealNumber, is EqualsSymbol, is ExponentiationSymbol, + is InvalidToken, is MinusSymbol, is PlusSymbol, is RightParenthesisSymbol, + is IncompleteFunctionName, null -> null + } + } + } + + private fun parseGenericMultExpressionRhs(): MathParsingResult { + // generic_mult_expression_rhs = multiplication_operator , generic_exp_expression ; + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(MULTIPLY) + } else null + }.flatMap { + parseGenericExpExpression() + } + } + + private fun parseGenericDivExpressionRhs(): MathParsingResult { + // generic_div_expression_rhs = division_operator , generic_exp_expression ; + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(DIVIDE) + } else null + }.flatMap { + parseGenericExpExpression() + } + } + + private fun parseGenericImplicitMultExpressionRhs(): MathParsingResult { + // generic_implicit_mult_expression_rhs is either numeric_implicit_mult_expression_rhs or + // algebraic_implicit_mult_or_exp_expression_rhs depending on the current parser context. + return when (parseContext) { + is NumericExpressionContext -> parseNumericImplicitMultExpressionRhs() + is AlgebraicExpressionContext -> parseAlgebraicImplicitMultOrExpExpressionRhs() + } + } + + private fun parseNumericImplicitMultExpressionRhs(): MathParsingResult { + // numeric_implicit_mult_expression_rhs = generic_term_without_unary_without_number ; + return parseGenericTermWithoutUnaryWithoutNumber() + } + + private fun parseAlgebraicImplicitMultOrExpExpressionRhs(): MathParsingResult { + // algebraic_implicit_mult_or_exp_expression_rhs = + // generic_term_without_unary_without_number , [ generic_exp_expression_tail ] ; + val possibleLhs = parseGenericTermWithoutUnaryWithoutNumber() + return if (parseContext.hasNextTokenOfType()) { + parseGenericExpExpressionTail(possibleLhs) + } else possibleLhs + } + + private fun parseGenericExpExpression(): MathParsingResult { + // generic_exp_expression = generic_term_with_unary , [ generic_exp_expression_tail ] ; + val possibleLhs = parseGenericTermWithUnary() + return if (parseContext.hasNextTokenOfType()) { + parseGenericExpExpressionTail(possibleLhs) + } else possibleLhs + } + + // Use tail recursion so that the last exponentiation is evaluated first, and right-to-left + // associativity can be kept via backtracking. + private fun parseGenericExpExpressionTail( + lhsResult: MathParsingResult + ): MathParsingResult { + // generic_exp_expression_tail = exponentiation_operator , generic_exp_expression ; + return BinaryOperationRhs( + operator = EXPONENTIATE, + rhsResult = lhsResult.flatMap { + parseContext.consumeTokenOfType() + }.maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(EXPONENTIATE) + } else null + }.flatMap { + parseGenericExpExpression() + } + ).computeBinaryOperationExpression(lhsResult) + } + + private fun parseGenericTermWithUnary(): MathParsingResult { + // generic_term_with_unary = + // number | generic_term_without_unary_without_number | generic_plus_minus_unary_term ; + return when (val nextToken = parseContext.peekToken()) { + is MinusSymbol, is PlusSymbol -> parseGenericPlusMinusUnaryTerm() + is PositiveInteger, is PositiveRealNumber -> parseNumber().takeUnless { + parseContext.hasNextTokenOfType() || + parseContext.hasNextTokenOfType() + } ?: SpacesBetweenNumbersError.toFailure() + is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> + parseGenericTermWithoutUnaryWithoutNumber() + is VariableName -> { + if (parseContext is AlgebraicExpressionContext) { + parseGenericTermWithoutUnaryWithoutNumber() + } else VariableInNumericExpressionError.toFailure() + } + is DivideSymbol, is ExponentiationSymbol, is MultiplySymbol -> { + val previousToken = parseContext.getPreviousToken() + when { + previousToken is BinaryOperatorToken -> { + SubsequentBinaryOperatorsError( + operator1 = parseContext.extractSubexpression(previousToken), + operator2 = parseContext.extractSubexpression(nextToken) + ).toFailure() + } + nextToken is BinaryOperatorToken -> { + NoVariableOrNumberBeforeBinaryOperatorError( + operator = nextToken.getBinaryOperator() + ).toFailure() + } + else -> GenericError.toFailure() + } + } + is EqualsSymbol -> { + if (parseContext is AlgebraicExpressionContext && parseContext.isPartOfEquation) { + EquationHasWrongNumberOfEqualsError.toFailure() + } else GenericError.toFailure() + } + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + is RightParenthesisSymbol, null -> GenericError.toFailure() + } + } + + private fun parseGenericTermWithoutUnaryWithoutNumber(): MathParsingResult { + // generic_term_without_unary_without_number is either numeric_term_without_unary_without_number + // or algebraic_term_without_unary_without_number based the current parser context. + return when (parseContext) { + is NumericExpressionContext -> parseNumericTermWithoutUnaryWithoutNumber() + is AlgebraicExpressionContext -> parseAlgebraicTermWithoutUnaryWithoutNumber() + } + } + + private fun parseNumericTermWithoutUnaryWithoutNumber(): MathParsingResult { + // numeric_term_without_unary_without_number = + // generic_function_expression | generic_group_expression | generic_rooted_term ; + return when (val nextToken = parseContext.peekToken()) { + is FunctionName -> parseGenericFunctionExpression() + is LeftParenthesisSymbol -> parseGenericGroupExpression() + is SquareRootSymbol -> parseGenericRootedTerm() + is VariableName -> VariableInNumericExpressionError.toFailure() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, + is RightParenthesisSymbol, null -> GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + } + } + + private fun parseAlgebraicTermWithoutUnaryWithoutNumber(): MathParsingResult { + // algebraic_term_without_unary_without_number = + // generic_function_expression | generic_group_expression | generic_rooted_term | variable ; + return when (val nextToken = parseContext.peekToken()) { + is FunctionName -> parseGenericFunctionExpression() + is LeftParenthesisSymbol -> parseGenericGroupExpression() + is SquareRootSymbol -> parseGenericRootedTerm() + is VariableName -> parseVariable() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, + is RightParenthesisSymbol, null -> GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + } + } + + private fun parseGenericFunctionExpression(): MathParsingResult { + // generic_function_expression = function_name , left_paren , generic_expression , right_paren ; + val funcNameResult = + parseContext.consumeTokenOfType().maybeFail { functionName -> + when { + !functionName.isAllowedFunction -> InvalidFunctionInUseError(functionName.parsedName) + functionName.parsedName == "sqrt" -> null + else -> GenericError + } + }.also { + parseContext.consumeTokenOfType() + } + val argResult = funcNameResult.flatMap { parseGenericExpression() } + val rightParenResult = + argResult.flatMap { + parseContext.consumeTokenOfType { UnbalancedParenthesesError } + } + return funcNameResult.combineWith(argResult, rightParenResult) { funcName, arg, rightParen -> + MathExpression.newBuilder().apply { + parseStartIndex = funcName.startIndex + parseEndIndex = rightParen.endIndex + functionCall = MathFunctionCall.newBuilder().apply { + functionType = MathFunctionCall.FunctionType.SQUARE_ROOT + argument = arg + }.build() + }.build() + } + } + + private fun parseGenericGroupExpression(): MathParsingResult { + // generic_group_expression = left_paren , generic_expression , right_paren ; + val leftParenResult = parseContext.consumeTokenOfType() + val expResult = + leftParenResult.flatMap { + if (parseContext.hasMoreTokens()) { + parseGenericExpression() + } else UnbalancedParenthesesError.toFailure() + } + val rightParenResult = + expResult.flatMap { + parseContext.consumeTokenOfType { UnbalancedParenthesesError } + } + return leftParenResult.combineWith(expResult, rightParenResult) { leftParen, exp, rightParen -> + MathExpression.newBuilder().apply { + parseStartIndex = leftParen.startIndex + parseEndIndex = rightParen.endIndex + group = exp + }.build() + } + } + + private fun parseGenericPlusMinusUnaryTerm(): MathParsingResult { + // generic_plus_minus_unary_term = generic_negated_term | generic_positive_term ; + return when (val nextToken = parseContext.peekToken()) { + is MinusSymbol -> parseGenericNegatedTerm() + is PlusSymbol -> parseGenericPositiveTerm() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is FunctionName, is LeftParenthesisSymbol, is MultiplySymbol, + is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, null -> + GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + } + } + + private fun parseGenericNegatedTerm(): MathParsingResult { + // generic_negated_term = minus_operator , generic_mult_div_expression ; + val minusResult = parseContext.consumeTokenOfType() + val expResult = minusResult.flatMap { parseGenericMultDivExpression() } + return minusResult.combineWith(expResult) { minus, op -> + MathExpression.newBuilder().apply { + parseStartIndex = minus.startIndex + parseEndIndex = op.parseEndIndex + unaryOperation = MathUnaryOperation.newBuilder().apply { + operator = NEGATE + operand = op + }.build() + }.build() + } + } + + private fun parseGenericPositiveTerm(): MathParsingResult { + // generic_positive_term = plus_operator , generic_mult_div_expression ; + val plusResult = parseContext.consumeTokenOfType() + val expResult = plusResult.flatMap { parseGenericMultDivExpression() } + return plusResult.combineWith(expResult) { plus, op -> + MathExpression.newBuilder().apply { + parseStartIndex = plus.startIndex + parseEndIndex = op.parseEndIndex + unaryOperation = MathUnaryOperation.newBuilder().apply { + operator = POSITIVE + operand = op + }.build() + }.build() + } + } + + private fun parseGenericRootedTerm(): MathParsingResult { + // generic_rooted_term = square_root_operator , generic_term_with_unary ; + val sqrtResult = + parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) HangingSquareRootError else null + } + val expResult = sqrtResult.flatMap { parseGenericTermWithUnary() } + return sqrtResult.combineWith(expResult) { sqrtSymbol, op -> + MathExpression.newBuilder().apply { + parseStartIndex = sqrtSymbol.startIndex + parseEndIndex = op.parseEndIndex + functionCall = MathFunctionCall.newBuilder().apply { + functionType = MathFunctionCall.FunctionType.SQUARE_ROOT + argument = op + }.build() + }.build() + } + } + + private fun parseNumber(): MathParsingResult { + // number = positive_real_number | positive_integer ; + return when (val nextToken = parseContext.peekToken()) { + is PositiveInteger -> { + parseContext.consumeTokenOfType().map { positiveInteger -> + MathExpression.newBuilder().apply { + parseStartIndex = positiveInteger.startIndex + parseEndIndex = positiveInteger.endIndex + constant = positiveInteger.toReal() + }.build() + } + } + is PositiveRealNumber -> { + parseContext.consumeTokenOfType().map { positiveRealNumber -> + MathExpression.newBuilder().apply { + parseStartIndex = positiveRealNumber.startIndex + parseEndIndex = positiveRealNumber.endIndex + constant = positiveRealNumber.toReal() + }.build() + } + } + is DivideSymbol, is EqualsSymbol, is ExponentiationSymbol, is FunctionName, + is LeftParenthesisSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, + is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, null -> + GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + } + } + + private fun parseVariable(): MathParsingResult { + val variableNameResult = + parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.allowsVariables()) GenericError else null + }.maybeFail { variableName -> + return@maybeFail if (parseContext.hasMoreTokens()) { + when (val nextToken = parseContext.peekToken()) { + is PositiveInteger -> + NumberAfterVariableError(nextToken.toReal(), variableName.parsedName) + is PositiveRealNumber -> + NumberAfterVariableError(nextToken.toReal(), variableName.parsedName) + else -> null + } + } else null + } + return variableNameResult.map { variableName -> + MathExpression.newBuilder().apply { + parseStartIndex = variableName.startIndex + parseEndIndex = variableName.endIndex + variable = variableName.parsedName + }.build() + } + } + + private fun parseGenericBinaryExpression( + parseLhs: () -> MathParsingResult, + parseRhs: (Token?) -> BinaryOperationRhs? + ): MathParsingResult { + var lastLhsResult = parseLhs() + while (!lastLhsResult.isFailure()) { + // Compute the next LHS if there are further RHS expressions. + lastLhsResult = + parseRhs(parseContext.peekToken()) + ?.computeBinaryOperationExpression(lastLhsResult) + ?: break // Not a match to the expression. + } + return lastLhsResult + } + + private fun checkForLearnerErrors(expression: MathExpression): MathParsingError? { + val firstMultiRedundantGroup = expression.findFirstMultiRedundantGroup() + val nextRedundantGroup = expression.findNextRedundantGroup() + val nextUnaryOperation = expression.findNextRedundantUnaryOperation() + val nextExpWithVariableExp = expression.findNextExponentiationWithVariablePower() + val nextExpWithTooLargePower = expression.findNextExponentiationWithTooLargePower() + val nextExpWithNestedExp = expression.findNextNestedExponentiation() + val nextDivByZero = expression.findNextDivisionByZero() + val disallowedVariables = expression.findAllDisallowedVariables(parseContext) + // Note that the order of checks here is important since errors have precedence, and some are + // redundant and, in the wrong order, may cause the wrong error to be returned. + val includeOptionalErrors = parseContext.errorCheckingMode.includesOptionalErrors() + return when { + includeOptionalErrors && firstMultiRedundantGroup != null -> { + val subExpression = parseContext.extractSubexpression(firstMultiRedundantGroup) + MultipleRedundantParenthesesError(subExpression, firstMultiRedundantGroup) + } + includeOptionalErrors && expression.expressionTypeCase == GROUP -> + SingleRedundantParenthesesError(parseContext.rawExpression, expression) + includeOptionalErrors && nextRedundantGroup != null -> { + val subExpression = parseContext.extractSubexpression(nextRedundantGroup) + RedundantParenthesesForIndividualTermsError(subExpression, nextRedundantGroup) + } + includeOptionalErrors && nextUnaryOperation != null -> SubsequentUnaryOperatorsError + includeOptionalErrors && nextExpWithVariableExp != null -> ExponentIsVariableExpressionError + includeOptionalErrors && nextExpWithTooLargePower != null -> ExponentTooLargeError + includeOptionalErrors && nextExpWithNestedExp != null -> NestedExponentsError + includeOptionalErrors && nextDivByZero != null -> TermDividedByZeroError + includeOptionalErrors && disallowedVariables.isNotEmpty() -> + DisabledVariablesInUseError(disallowedVariables.toList()) + else -> ensureNoRemainingTokens() + } + } + + private fun ensureNoRemainingTokens(): MathParsingError? { + // Make sure all tokens were consumed (otherwise there are trailing tokens which invalidate the + // whole grammar). + return if (parseContext.hasMoreTokens()) { + when (val nextToken = parseContext.peekToken()) { + is LeftParenthesisSymbol, is RightParenthesisSymbol -> UnbalancedParenthesesError + is EqualsSymbol -> { + if (parseContext is AlgebraicExpressionContext && parseContext.isPartOfEquation) { + EquationHasWrongNumberOfEqualsError + } else GenericError + } + is IncompleteFunctionName -> nextToken.toError() + is InvalidToken -> nextToken.toError() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is ExponentiationSymbol, + is FunctionName, is MinusSymbol, is MultiplySymbol, is PlusSymbol, is SquareRootSymbol, + is VariableName, null -> GenericError + } + } else null + } + + private fun PositiveInteger.toReal(): Real = Real.newBuilder().apply { + integer = parsedValue + }.build() + + private fun PositiveRealNumber.toReal(): Real = Real.newBuilder().apply { + irrational = parsedValue + }.build() + + @Suppress("unused") // The receiver is behaving as a namespace. + private fun IncompleteFunctionName.toError(): MathParsingError = FunctionNameIncompleteError + + private fun InvalidToken.toError(): MathParsingError = + UnnecessarySymbolsError(parseContext.extractSubexpression(this)) + + private fun IncompleteFunctionName.toFailure(): MathParsingResult = toError().toFailure() + + private fun InvalidToken.toFailure(): MathParsingResult = toError().toFailure() + + private sealed class ParseContext(val rawExpression: String) { + val tokens: PeekableIterator by lazy { + PeekableIterator.fromSequence(MathTokenizer.tokenize(rawExpression)) + } + private var previousToken: Token? = null + + abstract val errorCheckingMode: ErrorCheckingMode + + abstract fun allowsVariables(): Boolean + + fun hasMoreTokens(): Boolean = tokens.hasNext() + + fun peekToken(): Token? = tokens.peek() + + /** + * Returns the last token consumed by [consumeTokenOfType], or null if none. Note: this should + * only be used for error reporting purposes, not for parsing. Using this for parsing would, in + * certain cases, allow for a non-LL(1) grammar which is against one design goal for this + * parser. + */ + fun getPreviousToken(): Token? = previousToken + + inline fun hasNextTokenOfType(): Boolean = peekToken() is T + + inline fun consumeTokenOfType( + missingError: () -> MathParsingError = { GenericError } + ): MathParsingResult { + val maybeToken = tokens.expectNextMatches { it is T } as? T + return maybeToken?.let { token -> + previousToken = token + MathParsingResult.Success(token) + } ?: missingError().toFailure() + } + + fun extractSubexpression(token: Token): String { + return rawExpression.substring(token.startIndex, token.endIndex) + } + + fun extractSubexpression(expression: MathExpression): String { + return rawExpression.substring(expression.parseStartIndex, expression.parseEndIndex) + } + + class NumericExpressionContext( + rawExpression: String, + override val errorCheckingMode: ErrorCheckingMode + ) : ParseContext(rawExpression) { + // Numeric expressions never allow variables. + override fun allowsVariables(): Boolean = false + } + + class AlgebraicExpressionContext( + rawExpression: String, + val isPartOfEquation: Boolean, + private val allowedVariables: List, + override val errorCheckingMode: ErrorCheckingMode + ) : ParseContext(rawExpression) { + fun allowsVariable(variableName: String): Boolean = variableName in allowedVariables + + override fun allowsVariables(): Boolean = true + } + } + + companion object { + enum class ErrorCheckingMode { + REQUIRED_ONLY, + ALL_ERRORS + } + + sealed class MathParsingResult { + data class Success(val result: T) : MathParsingResult() + + data class Failure(val error: MathParsingError) : MathParsingResult() + } + + fun parseNumericExpression( + rawExpression: String, + errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS + ): MathParsingResult = + createNumericParser(rawExpression, errorCheckingMode).parseGenericExpressionGrammar() + + fun parseAlgebraicExpression( + rawExpression: String, + allowedVariables: List, + errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS + ): MathParsingResult { + return createAlgebraicParser( + rawExpression, isPartOfEquation = false, allowedVariables, errorCheckingMode + ).parseGenericExpressionGrammar() + } + + fun parseAlgebraicEquation( + rawExpression: String, + allowedVariables: List, + errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS + ): MathParsingResult { + return createAlgebraicParser( + rawExpression, isPartOfEquation = true, allowedVariables, errorCheckingMode + ).parseGenericEquationGrammar() + } + + private fun createNumericParser( + rawExpression: String, + errorCheckingMode: ErrorCheckingMode + ): MathExpressionParser = + MathExpressionParser(NumericExpressionContext(rawExpression, errorCheckingMode)) + + private fun createAlgebraicParser( + rawExpression: String, + isPartOfEquation: Boolean, + allowedVariables: List, + errorCheckingMode: ErrorCheckingMode + ): MathExpressionParser { + return MathExpressionParser( + AlgebraicExpressionContext( + rawExpression, isPartOfEquation, allowedVariables, errorCheckingMode + ) + ) + } + + private fun ErrorCheckingMode.includesOptionalErrors() = this == ErrorCheckingMode.ALL_ERRORS + + private fun MathParsingError.toFailure(): MathParsingResult = + MathParsingResult.Failure(this) + + private fun MathParsingResult.isFailure() = this is MathParsingResult.Failure + + /** + * Maps [this] result to a new value. Note that this lazily uses the provided function (i.e. + * it's only used if [this] result is passing, otherwise the method will short-circuit a failure + * state so that [this] result's failure is preserved). + * + * @param operation computes a new success result given the current successful result value + * @return a new [MathParsingResult] with a successful result provided by the operation, or the + * preserved failure of [this] result + */ + private fun MathParsingResult.map( + operation: (T1) -> T2 + ): MathParsingResult = flatMap { result -> MathParsingResult.Success(operation(result)) } + + /** + * Maps [this] result to a new value. Note that this lazily uses the provided function (i.e. + * it's only used if [this] result is passing, otherwise the method will short-circuit a failure + * state so that [this] result's failure is preserved). + * + * @param operation computes a new result (either a success or failure) given the current + * successful result value + * @return a new [MathParsingResult] with either a result provided by the operation, or the + * preserved failure of [this] result + */ + private fun MathParsingResult.flatMap( + operation: (T1) -> MathParsingResult + ): MathParsingResult { + return when (this) { + is MathParsingResult.Success -> operation(result) + is MathParsingResult.Failure -> error.toFailure() + } + } + + /** + * Potentially changes [this] result into a failure based on the provided [operation]. Note that + * this function lazily uses the operation (i.e. it's only called if [this] result is in a + * passing state), and the returned result will only be in a failing state if [operation] + * returns a non-null error. + * + * @param operation computes a failure error, or null if no error was determined, given the + * current successful result value + * @return either [this] or a failing result if [operation] was called & returned a non-null + * error + */ + private fun MathParsingResult.maybeFail( + operation: (T) -> MathParsingError? + ): MathParsingResult = flatMap { result -> operation(result)?.toFailure() ?: this } + + /** + * Calls an operation if [this] operation isn't already failing, and returns a failure only if + * that operation's result is a failure (otherwise returns [this] result). This function can be + * useful to ensure that subsequent operations are successful even when those operations' + * results are never directly used. + * + * @param operation computes a new result that, when failing, will result in a failing result + * returned from this function. This is only called if [this] result is currently + * successful. + * @return either [this] (iff either this result is failing, or the result of [operation] is a + * success), or the failure returned by [operation] + */ + private fun MathParsingResult.also( + operation: () -> MathParsingResult + ): MathParsingResult = flatMap { + when (val other = operation()) { + is MathParsingResult.Success -> this + is MathParsingResult.Failure -> other.error.toFailure() + } + } + + /** + * Combines [this] result with another result, given a specific combination function. + * + * @param other the result to combine with [this] result + * @param combine computes a new value given the result from [this] and [other]. Note that this + * is only called if both results are successful, and the corresponding successful values + * are provided in-order ([this] result's value is the first parameter, and [other]'s is the + * second). + * @return either [this] result's or [other]'s failure, if either are failing, or a successful + * result containing the value computed by [combine] + */ + private fun MathParsingResult.combineWith( + other: MathParsingResult, + combine: (I1, I2) -> O, + ): MathParsingResult { + return flatMap { result -> + other.map { otherResult -> + combine(result, otherResult) + } + } + } + + /** + * Performs the same operation as the other [combineWith] function, except with three + * [MathParsingResult]s, instead. + */ + private fun MathParsingResult.combineWith( + other1: MathParsingResult, + other2: MathParsingResult, + combine: (I1, I2, I3) -> O, + ): MathParsingResult { + return flatMap { result -> + other1.flatMap { otherResult1 -> + other2.map { otherResult2 -> + combine(result, otherResult1, otherResult2) + } + } + } + } + + private data class BinaryOperationRhs( + val operator: MathBinaryOperation.Operator, + val rhsResult: MathParsingResult, + val isImplicit: Boolean = false + ) { + fun computeBinaryOperationExpression( + lhsResult: MathParsingResult + ): MathParsingResult { + return lhsResult.combineWith(rhsResult) { lhs, rhs -> + MathExpression.newBuilder().apply { + parseStartIndex = lhs.parseStartIndex + parseEndIndex = rhs.parseEndIndex + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = this@BinaryOperationRhs.operator + leftOperand = lhs + rightOperand = rhs + isImplicit = this@BinaryOperationRhs.isImplicit + }.build() + }.build() + } + } + } + + private fun MathExpression.findFirstMultiRedundantGroup(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findFirstMultiRedundantGroup() + ?: binaryOperation.rightOperand.findFirstMultiRedundantGroup() + } + UNARY_OPERATION -> unaryOperation.operand.findFirstMultiRedundantGroup() + FUNCTION_CALL -> functionCall.argument.findFirstMultiRedundantGroup() + GROUP -> + group.takeIf { it.expressionTypeCase == GROUP } + ?: group.findFirstMultiRedundantGroup() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextRedundantGroup(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findNextRedundantGroup() + ?: binaryOperation.rightOperand.findNextRedundantGroup() + } + UNARY_OPERATION -> unaryOperation.operand.findNextRedundantGroup() + FUNCTION_CALL -> functionCall.argument.findNextRedundantGroup() + GROUP -> group.takeIf { + it.expressionTypeCase in listOf(CONSTANT, VARIABLE) + } ?: group.findNextRedundantGroup() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextRedundantUnaryOperation(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findNextRedundantUnaryOperation() + ?: binaryOperation.rightOperand.findNextRedundantUnaryOperation() + } + UNARY_OPERATION -> unaryOperation.operand.takeIf { + it.expressionTypeCase == UNARY_OPERATION + } ?: unaryOperation.operand.findNextRedundantUnaryOperation() + FUNCTION_CALL -> functionCall.argument.findNextRedundantUnaryOperation() + GROUP -> group.findNextRedundantUnaryOperation() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextExponentiationWithVariablePower(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == EXPONENTIATE && + binaryOperation.rightOperand.isVariableExpression() + } ?: binaryOperation.leftOperand.findNextExponentiationWithVariablePower() + ?: binaryOperation.rightOperand.findNextExponentiationWithVariablePower() + } + UNARY_OPERATION -> unaryOperation.operand.findNextExponentiationWithVariablePower() + FUNCTION_CALL -> functionCall.argument.findNextExponentiationWithVariablePower() + GROUP -> group.findNextExponentiationWithVariablePower() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextExponentiationWithTooLargePower(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == EXPONENTIATE && + binaryOperation.rightOperand.expressionTypeCase == CONSTANT && + binaryOperation.rightOperand.constant.toDouble() > 5.0 + } ?: binaryOperation.leftOperand.findNextExponentiationWithTooLargePower() + ?: binaryOperation.rightOperand.findNextExponentiationWithTooLargePower() + } + UNARY_OPERATION -> unaryOperation.operand.findNextExponentiationWithTooLargePower() + FUNCTION_CALL -> functionCall.argument.findNextExponentiationWithTooLargePower() + GROUP -> group.findNextExponentiationWithTooLargePower() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextNestedExponentiation(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == EXPONENTIATE && + binaryOperation.rightOperand.containsExponentiation() + } ?: binaryOperation.leftOperand.findNextNestedExponentiation() + ?: binaryOperation.rightOperand.findNextNestedExponentiation() + } + UNARY_OPERATION -> unaryOperation.operand.findNextNestedExponentiation() + FUNCTION_CALL -> functionCall.argument.findNextNestedExponentiation() + GROUP -> group.findNextNestedExponentiation() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextDivisionByZero(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == DIVIDE && + binaryOperation.rightOperand.expressionTypeCase == CONSTANT && + binaryOperation.rightOperand.constant + .toDouble().absoluteValue.approximatelyEquals(0.0) + } ?: binaryOperation.leftOperand.findNextDivisionByZero() + ?: binaryOperation.rightOperand.findNextDivisionByZero() + } + UNARY_OPERATION -> unaryOperation.operand.findNextDivisionByZero() + FUNCTION_CALL -> functionCall.argument.findNextDivisionByZero() + GROUP -> group.findNextDivisionByZero() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findAllDisallowedVariables(context: ParseContext): Set { + return if (context is AlgebraicExpressionContext) { + findAllDisallowedVariablesAux(context) + } else setOf() + } + + private fun MathExpression.findAllDisallowedVariablesAux( + context: AlgebraicExpressionContext + ): Set { + return when (expressionTypeCase) { + VARIABLE -> if (context.allowsVariable(variable)) setOf() else setOf(variable) + BINARY_OPERATION -> { + binaryOperation.leftOperand.findAllDisallowedVariablesAux(context) + + binaryOperation.rightOperand.findAllDisallowedVariablesAux(context) + } + UNARY_OPERATION -> unaryOperation.operand.findAllDisallowedVariablesAux(context) + FUNCTION_CALL -> functionCall.argument.findAllDisallowedVariablesAux(context) + GROUP -> group.findAllDisallowedVariablesAux(context) + CONSTANT, EXPRESSIONTYPE_NOT_SET, null -> setOf() + } + } + + private fun MathExpression.isVariableExpression(): Boolean { + return when (expressionTypeCase) { + VARIABLE -> true + BINARY_OPERATION -> { + binaryOperation.leftOperand.isVariableExpression() || + binaryOperation.rightOperand.isVariableExpression() + } + UNARY_OPERATION -> unaryOperation.operand.isVariableExpression() + FUNCTION_CALL -> functionCall.argument.isVariableExpression() + GROUP -> group.isVariableExpression() + CONSTANT, EXPRESSIONTYPE_NOT_SET, null -> false + } + } + + private fun MathExpression.containsExponentiation(): Boolean { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.operator == EXPONENTIATE || + binaryOperation.leftOperand.containsExponentiation() || + binaryOperation.rightOperand.containsExponentiation() + } + UNARY_OPERATION -> unaryOperation.operand.containsExponentiation() + FUNCTION_CALL -> functionCall.argument.containsExponentiation() + GROUP -> group.containsExponentiation() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> false + } + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt new file mode 100644 index 00000000000..44fd1debb4a --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt @@ -0,0 +1,69 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.Real + +sealed class MathParsingError { + object SpacesBetweenNumbersError : MathParsingError() + + object UnbalancedParenthesesError : MathParsingError() + + data class SingleRedundantParenthesesError( + val rawExpression: String, + val expression: MathExpression + ) : MathParsingError() + + data class MultipleRedundantParenthesesError( + val rawExpression: String, + val expression: MathExpression + ) : MathParsingError() + + data class RedundantParenthesesForIndividualTermsError( + val rawExpression: String, + val expression: MathExpression + ) : MathParsingError() + + data class UnnecessarySymbolsError(val invalidSymbol: String) : MathParsingError() + + data class NumberAfterVariableError(val number: Real, val variable: String) : MathParsingError() + + data class SubsequentBinaryOperatorsError( + val operator1: String, + val operator2: String + ) : MathParsingError() + + object SubsequentUnaryOperatorsError : MathParsingError() + + data class NoVariableOrNumberBeforeBinaryOperatorError( + val operator: MathBinaryOperation.Operator + ) : MathParsingError() + + data class NoVariableOrNumberAfterBinaryOperatorError( + val operator: MathBinaryOperation.Operator + ) : MathParsingError() + + object ExponentIsVariableExpressionError : MathParsingError() + + object ExponentTooLargeError : MathParsingError() + + object NestedExponentsError : MathParsingError() + + object HangingSquareRootError : MathParsingError() + + object TermDividedByZeroError : MathParsingError() + + object VariableInNumericExpressionError : MathParsingError() + + data class DisabledVariablesInUseError(val variables: List) : MathParsingError() + + object EquationHasWrongNumberOfEqualsError : MathParsingError() + + object EquationMissingLhsOrRhsError : MathParsingError() + + data class InvalidFunctionInUseError(val functionName: String) : MathParsingError() + + object FunctionNameIncompleteError : MathParsingError() + + object GenericError : MathParsingError() +} diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt new file mode 100644 index 00000000000..94fc6e50ab4 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt @@ -0,0 +1,227 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathEquation +import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class AlgebraicEquationParserTest { + @Test + fun testLotsOfCasesForAlgebraicEquation() { + expectFailureWhenParsingAlgebraicEquation(" x =") + expectFailureWhenParsingAlgebraicEquation(" = y") + + val equation1 = parseAlgebraicEquationSuccessfully("x = 1") + assertThat(equation1).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("x") + } + } + + val equation2 = + parseAlgebraicEquationSuccessfully( + "y = mx + b", allowedVariables = listOf("x", "y", "b", "m") + ) + assertThat(equation2).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + assertThat(equation2).hasRightHandSideThat().hasStructureThatMatches { + addition { + leftOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("m") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + rightOperand { + variable { + withNameThat().isEqualTo("b") + } + } + } + } + + val equation3 = parseAlgebraicEquationSuccessfully("y = (x+1)^2") + assertThat(equation3).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + assertThat(equation3).hasRightHandSideThat().hasStructureThatMatches { + exponentiation { + leftOperand { + group { + addition { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + val equation4 = parseAlgebraicEquationSuccessfully("y = (x+1)(x-1)") + assertThat(equation4).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + assertThat(equation4).hasRightHandSideThat().hasStructureThatMatches { + multiplication { + leftOperand { + group { + addition { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + } + } + + expectFailureWhenParsingAlgebraicEquation("y = (x+1)(x-1) 2") + expectFailureWhenParsingAlgebraicEquation("y 2 = (x+1)(x-1)") + + val equation5 = + parseAlgebraicEquationSuccessfully( + "a*x^2 + b*x + c = 0", allowedVariables = listOf("x", "a", "b", "c") + ) + assertThat(equation5).hasLeftHandSideThat().hasStructureThatMatches { + addition { + leftOperand { + addition { + leftOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("a") + } + } + rightOperand { + exponentiation { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("b") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + } + } + rightOperand { + variable { + withNameThat().isEqualTo("c") + } + } + } + } + assertThat(equation5).hasRightHandSideThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(0) + } + } + } + + private companion object { + // TODO: fix helper API. + + private fun expectFailureWhenParsingAlgebraicEquation(expression: String): MathParsingError { + val result = parseAlgebraicEquationWithAllErrors(expression) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseAlgebraicEquationSuccessfully( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathEquation { + val result = parseAlgebraicEquationWithAllErrors(expression, allowedVariables) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicEquationWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicEquation( + expression, allowedVariables, ErrorCheckingMode.ALL_ERRORS + ) + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt new file mode 100644 index 00000000000..9fea4084970 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt @@ -0,0 +1,1939 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class AlgebraicExpressionParserTest { + @Test + fun testLotsOfCasesForAlgebraicExpression() { + // TODO: split this up + // TODO: add log string generation for expressions. + expectFailureWhenParsingAlgebraicExpression("") + + val expression1 = parseAlgebraicExpressionWithAllErrors("1") + assertThat(expression1).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + + val expression61 = parseAlgebraicExpressionWithAllErrors("x") + assertThat(expression61).hasStructureThatMatches { + variable { + withNameThat().isEqualTo("x") + } + } + + val expression2 = parseAlgebraicExpressionWithAllErrors(" 2 ") + assertThat(expression2).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + + val expression3 = parseAlgebraicExpressionWithAllErrors(" 2.5 ") + assertThat(expression3).hasStructureThatMatches { + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) + } + } + + val expression62 = parseAlgebraicExpressionWithAllErrors(" y ") + assertThat(expression62).hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + + val expression63 = parseAlgebraicExpressionWithAllErrors(" z x ") + assertThat(expression63).hasStructureThatMatches { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("z") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + + val expression4 = parseAlgebraicExpressionWithoutOptionalErrors("2^3^2") + assertThat(expression4).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression23 = parseAlgebraicExpressionWithAllErrors("(2^3)^2") + assertThat(expression23).hasStructureThatMatches { + exponentiation { + leftOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + val expression24 = parseAlgebraicExpressionWithAllErrors("512/32/4") + assertThat(expression24).hasStructureThatMatches { + division { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(512) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(32) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val expression25 = parseAlgebraicExpressionWithAllErrors("512/(32/4)") + assertThat(expression25).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(512) + } + } + rightOperand { + group { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(32) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + val expression5 = parseAlgebraicExpressionWithAllErrors("sqrt(2)") + assertThat(expression5).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + expectFailureWhenParsingAlgebraicExpression("sqr(2)") + + val expression64 = parseAlgebraicExpressionWithoutOptionalErrors("xyz(2)") + assertThat(expression64).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("y") + } + } + } + } + rightOperand { + variable { + withNameThat().isEqualTo("z") + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression6 = parseAlgebraicExpressionWithAllErrors("732") + assertThat(expression6).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(732) + } + } + + expectFailureWhenParsingAlgebraicExpression("73 2") + + // Verify order of operations between higher & lower precedent operators. + val expression32 = parseAlgebraicExpressionWithAllErrors("3+4^5") + assertThat(expression32).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + + val expression7 = parseAlgebraicExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") + assertThat(expression7).hasStructureThatMatches { + // To better visualize the precedence & order of operations, see this grouped version: + // (((3*2)-3)+((((4^7)*8)/3)*2))+7. + addition { + leftOperand { + // ((3*2)-3)+((((4^7)*8)/3)*2) + addition { + leftOperand { + // (1*2)-3 + subtraction { + leftOperand { + // 3*2 + multiplication { + leftOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // (((4^7)*8)/3)*2 + multiplication { + leftOperand { + // ((4^7)*8)/3 + division { + leftOperand { + // (4^7)*8 + multiplication { + leftOperand { + // 4^7 + exponentiation { + leftOperand { + // 4 + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + rightOperand { + // 8 + constant { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + + expectFailureWhenParsingAlgebraicExpression("x = √2 × 7 ÷ 4") + + val expression8 = parseAlgebraicExpressionWithAllErrors("(1+2)(3+4)") + assertThat(expression8).hasStructureThatMatches { + multiplication { + leftOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsingAlgebraicExpression("(1+2)2") + + val expression10 = parseAlgebraicExpressionWithAllErrors("2(1+2)") + assertThat(expression10).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsingAlgebraicExpression("sqrt(2)3") + + val expression12 = parseAlgebraicExpressionWithAllErrors("3sqrt(2)") + assertThat(expression12).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression65 = parseAlgebraicExpressionWithAllErrors("xsqrt(2)") + assertThat(expression65).hasStructureThatMatches { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression13 = parseAlgebraicExpressionWithAllErrors("sqrt(2)*(1+2)*(3-2^5)") + assertThat(expression13).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + } + } + } + + val expression58 = parseAlgebraicExpressionWithAllErrors("sqrt(2)(1+2)(3-2^5)") + assertThat(expression58).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + } + } + } + + val expression14 = parseAlgebraicExpressionWithoutOptionalErrors("((3))") + assertThat(expression14).hasStructureThatMatches { + group { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val expression15 = parseAlgebraicExpressionWithoutOptionalErrors("++3") + assertThat(expression15).hasStructureThatMatches { + positive { + operand { + positive { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + + val expression16 = parseAlgebraicExpressionWithoutOptionalErrors("--4") + assertThat(expression16).hasStructureThatMatches { + negation { + operand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression17 = parseAlgebraicExpressionWithAllErrors("1+-4") + assertThat(expression17).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression18 = parseAlgebraicExpressionWithAllErrors("1++4") + assertThat(expression18).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + positive { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression19 = parseAlgebraicExpressionWithAllErrors("1--4") + assertThat(expression19).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + expectFailureWhenParsingAlgebraicExpression("1-^-4") + + val expression20 = parseAlgebraicExpressionWithAllErrors("√2 × 7 ÷ 4") + assertThat(expression20).hasStructureThatMatches { + division { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + expectFailureWhenParsingAlgebraicExpression("1+2 &asdf") + + val expression21 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(3)sqrt(4)") + // Note that this tree demonstrates left associativity. + assertThat(expression21).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression22 = parseAlgebraicExpressionWithAllErrors("(1+2)(3-7^2)(5+-17)") + assertThat(expression22).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + // 1+2 + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + // 3-7^2 + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + rightOperand { + // 5+-17 + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(17) + } + } + } + } + } + } + } + } + } + + val expression26 = parseAlgebraicExpressionWithAllErrors("3^-2") + assertThat(expression26).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression27 = parseAlgebraicExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") + assertThat(expression27).hasStructureThatMatches { + exponentiation { + leftOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + + val expression28 = parseAlgebraicExpressionWithAllErrors("1-3^sqrt(4)") + assertThat(expression28).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + // "Hard" order of operation problems loosely based on & other problems that can often stump + // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. + val expression29 = parseAlgebraicExpressionWithAllErrors("3÷2*(3+4)") + assertThat(expression29).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + val expression59 = parseAlgebraicExpressionWithAllErrors("3÷2(3+4)") + assertThat(expression59).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + // Numbers cannot have implicit multiplication unless they are in groups. + expectFailureWhenParsingAlgebraicExpression("2 2") + + expectFailureWhenParsingAlgebraicExpression("2 2^2") + + expectFailureWhenParsingAlgebraicExpression("2^2 2") + + val expression31 = parseAlgebraicExpressionWithoutOptionalErrors("(3)(4)(5)") + assertThat(expression31).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + + val expression33 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)") + assertThat(expression33).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + // Verify that implicit multiple has lower precedence than exponentiation. + val expression34 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(4)") + assertThat(expression34).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsingAlgebraicExpression("2^(3)2^2") + + val expression35 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)*2^2") + assertThat(expression35).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + // An exponentiation can be a right operand of an implicit mult if it's grouped. + val expression36 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(2^2)") + assertThat(expression36).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsingAlgebraicExpression("2^3(4)2^3") + + val expression38 = parseAlgebraicExpressionWithoutOptionalErrors("2^3(4)*2^3") + assertThat(expression38).hasStructureThatMatches { + // 2^3(4)*2^3 + multiplication { + leftOperand { + // 2^3(4) + multiplication { + leftOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 4 + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + rightOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + + expectFailureWhenParsingAlgebraicExpression("2^2 2^2") + expectFailureWhenParsingAlgebraicExpression("(3) 2^2") + expectFailureWhenParsingAlgebraicExpression("sqrt(3) 2^2") + expectFailureWhenParsingAlgebraicExpression("√2 2^2") + expectFailureWhenParsingAlgebraicExpression("2^2 3") + + expectFailureWhenParsingAlgebraicExpression("-2 3") + + val expression39 = parseAlgebraicExpressionWithAllErrors("-(1+2)") + assertThat(expression39).hasStructureThatMatches { + negation { + operand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + // Should pass for algebra. + val expression66 = parseAlgebraicExpressionWithAllErrors("-2 x") + assertThat(expression66).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + } + } + + val expression40 = parseAlgebraicExpressionWithAllErrors("-2 (1+2)") + assertThat(expression40).hasStructureThatMatches { + // The negation happens last for parity with other common calculators. + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + + val expression41 = parseAlgebraicExpressionWithoutOptionalErrors("-2^3(4)") + assertThat(expression41).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + val expression43 = parseAlgebraicExpressionWithoutOptionalErrors("√2^2(3)") + assertThat(expression43).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + val expression60 = parseAlgebraicExpressionWithoutOptionalErrors("√(2^2(3))") + assertThat(expression60).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + group { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + } + + val expression42 = parseAlgebraicExpressionWithAllErrors("-2*-2") + // Note that the following structure is not the same as (-2)*(-2) since unary negation has + // higher precedence than multiplication, so it's first & recurses to include the entire + // multiplication expression. + assertThat(expression42).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + + val expression44 = parseAlgebraicExpressionWithoutOptionalErrors("2(2)") + assertThat(expression44).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression45 = parseAlgebraicExpressionWithAllErrors("2sqrt(2)") + assertThat(expression45).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression46 = parseAlgebraicExpressionWithAllErrors("2√2") + assertThat(expression46).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression47 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)") + assertThat(expression47).hasStructureThatMatches { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression48 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)(2)") + assertThat(expression48).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression49 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(2)") + assertThat(expression49).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression50 = parseAlgebraicExpressionWithAllErrors("√2√2") + assertThat(expression50).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression51 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)(2)") + assertThat(expression51).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression52 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(2)sqrt(2)") + assertThat(expression52).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression53 = parseAlgebraicExpressionWithAllErrors("√2√2√2") + assertThat(expression53).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + // Should fail for algebra. + expectFailureWhenParsingAlgebraicExpression("x7") + + // Should pass for algebra. + val expression67 = parseAlgebraicExpressionWithAllErrors("2x^2y^-3") + assertThat(expression67).hasStructureThatMatches { + // 2x^2y^-3 -> (2*(x^2))*(y^(-3)) + multiplication { + // 2x^2 + leftOperand { + multiplication { + // 2 + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + // x^2 + rightOperand { + exponentiation { + // x + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + // 2 + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + // y^-3 + rightOperand { + exponentiation { + // y + leftOperand { + variable { + withNameThat().isEqualTo("y") + } + } + // -3 + rightOperand { + negation { + // 3 + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + } + + val expression54 = parseAlgebraicExpressionWithAllErrors("2*2/-4+7*2") + assertThat(expression54).hasStructureThatMatches { + // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) + addition { + leftOperand { + // 2*2/-4 + division { + leftOperand { + // 2*2 + multiplication { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + // -4 + negation { + // 4 + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + rightOperand { + // 7*2 + multiplication { + leftOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression55 = parseAlgebraicExpressionWithAllErrors("3/(1-2)") + assertThat(expression55).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + val expression56 = parseAlgebraicExpressionWithoutOptionalErrors("(3)/(1-2)") + assertThat(expression56).hasStructureThatMatches { + division { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + val expression57 = parseAlgebraicExpressionWithoutOptionalErrors("3/((1-2))") + assertThat(expression57).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + group { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + + // TODO: add others, including tests for malformed expressions throughout the parser & + // tokenizer. + } + + private companion object { + // TODO: fix helper API. + + private fun expectFailureWhenParsingAlgebraicExpression( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingError { + val result = + parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseAlgebraicExpressionWithoutOptionalErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index d918580ffb9..2a8b0c295c4 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -4,6 +4,69 @@ TODO: document load("//:oppia_android_test.bzl", "oppia_android_test") +# "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_list_subject", +# "//testing/src/main/java/org/oppia/android/testing/math:fraction_subject", +# "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", +# "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", +# "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", +# "//testing/src/main/java/org/oppia/android/testing/math:real_subject", + +oppia_android_test( + name = "AlgebraicEquationParserTest", + srcs = ["AlgebraicEquationParserTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.AlgebraicEquationParserTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:parser", + ], +) + +oppia_android_test( + name = "AlgebraicExpressionParserTest", + srcs = ["AlgebraicExpressionParserTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.AlgebraicExpressionParserTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:parser", + ], +) + +oppia_android_test( + name = "MathExpressionParserTest", + srcs = ["MathExpressionParserTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.MathExpressionParserTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:parser", + ], +) + oppia_android_test( name = "MathTokenizerTest", srcs = ["MathTokenizerTest.kt"], @@ -22,6 +85,25 @@ oppia_android_test( ], ) +oppia_android_test( + name = "NumericExpressionParserTest", + srcs = ["NumericExpressionParserTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.NumericExpressionParserTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:parser", + ], +) + oppia_android_test( name = "RatioExtensionsTest", srcs = ["RatioExtensionsTest.kt"], diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt new file mode 100644 index 00000000000..3c50966b01e --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -0,0 +1,344 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError +import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError +import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError +import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError +import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError +import org.oppia.android.util.math.MathParsingError.FunctionNameIncompleteError +import org.oppia.android.util.math.MathParsingError.HangingSquareRootError +import org.oppia.android.util.math.MathParsingError.InvalidFunctionInUseError +import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.NestedExponentsError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberAfterBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberBeforeBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NumberAfterVariableError +import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError +import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError +import org.oppia.android.util.math.MathParsingError.SubsequentBinaryOperatorsError +import org.oppia.android.util.math.MathParsingError.SubsequentUnaryOperatorsError +import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError +import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError +import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError +import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class MathExpressionParserTest { + // TODO: add high-level checks for the three types, but don't test in detail since there are + // separate suites. Also, document the separate suites' existence in this suites's KDoc. + + @Test + fun testErrorCases() { + // TODO: split up. + val failure1 = expectFailureWhenParsingNumericExpression("73 2") + assertThat(failure1).isEqualTo(SpacesBetweenNumbersError) + + val failure2 = expectFailureWhenParsingNumericExpression("(73") + assertThat(failure2).isEqualTo(UnbalancedParenthesesError) + + val failure3 = expectFailureWhenParsingNumericExpression("73)") + assertThat(failure3).isEqualTo(UnbalancedParenthesesError) + + val failure4 = expectFailureWhenParsingNumericExpression("((73)") + assertThat(failure4).isEqualTo(UnbalancedParenthesesError) + + val failure5 = expectFailureWhenParsingNumericExpression("73 (") + assertThat(failure5).isEqualTo(UnbalancedParenthesesError) + + val failure6 = expectFailureWhenParsingNumericExpression("73 )") + assertThat(failure6).isEqualTo(UnbalancedParenthesesError) + + val failure7 = expectFailureWhenParsingNumericExpression("sqrt(73") + assertThat(failure7).isEqualTo(UnbalancedParenthesesError) + + // TODO: test properties on errors (& add better testing library for errors, or at least helpers). + val failure8 = expectFailureWhenParsingNumericExpression("(7 * 2 + 4)") + assertThat(failure8).isInstanceOf(SingleRedundantParenthesesError::class.java) + + val failure9 = expectFailureWhenParsingNumericExpression("((5 + 4))") + assertThat(failure9).isInstanceOf(MultipleRedundantParenthesesError::class.java) + + val failure13 = expectFailureWhenParsingNumericExpression("(((5 + 4)))") + assertThat(failure13).isInstanceOf(MultipleRedundantParenthesesError::class.java) + + val failure14 = expectFailureWhenParsingNumericExpression("1+((5 + 4))") + assertThat(failure14).isInstanceOf(MultipleRedundantParenthesesError::class.java) + + val failure15 = expectFailureWhenParsingNumericExpression("1+(7*((( 9 + 3) )))") + assertThat(failure15).isInstanceOf(MultipleRedundantParenthesesError::class.java) + assertThat((failure15 as MultipleRedundantParenthesesError).rawExpression) + .isEqualTo("(( 9 + 3) )") + + parseNumericExpressionSuccessfully("1+(5+4)") + parseNumericExpressionSuccessfully("(5+4)+1") + + val failure10 = expectFailureWhenParsingNumericExpression("(5) + 4") + assertThat(failure10).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + + val failure11 = expectFailureWhenParsingNumericExpression("5^(2)") + assertThat(failure11).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + assertThat((failure11 as RedundantParenthesesForIndividualTermsError).rawExpression) + .isEqualTo("2") + + val failure12 = expectFailureWhenParsingNumericExpression("sqrt((2))") + assertThat(failure12).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + + val failure16 = expectFailureWhenParsingNumericExpression("$2") + assertThat(failure16).isInstanceOf(UnnecessarySymbolsError::class.java) + assertThat((failure16 as UnnecessarySymbolsError).invalidSymbol).isEqualTo("$") + + val failure17 = expectFailureWhenParsingNumericExpression("5%") + assertThat(failure17).isInstanceOf(UnnecessarySymbolsError::class.java) + assertThat((failure17 as UnnecessarySymbolsError).invalidSymbol).isEqualTo("%") + + val failure18 = expectFailureWhenParsingAlgebraicExpression("x5") + assertThat(failure18).isInstanceOf(NumberAfterVariableError::class.java) + assertThat((failure18 as NumberAfterVariableError).number.integer).isEqualTo(5) + assertThat(failure18.variable).isEqualTo("x") + + val failure19 = expectFailureWhenParsingAlgebraicExpression("2+y 3.14*7") + assertThat(failure19).isInstanceOf(NumberAfterVariableError::class.java) + assertThat((failure19 as NumberAfterVariableError).number.irrational).isWithin(1e-5).of(3.14) + assertThat(failure19.variable).isEqualTo("y") + + // TODO: expand to multiple tests or use parametrized tests. + // RHS operators don't result in unary operations (which are valid in the grammar). + val rhsOperators = listOf("*", "×", "/", "÷", "^") + val lhsOperators = rhsOperators + listOf("+", "-", "−") + val operatorCombinations = lhsOperators.flatMap { op1 -> rhsOperators.map { op1 to it } } + for ((op1, op2) in operatorCombinations) { + val failure22 = expectFailureWhenParsingNumericExpression(expression = "1 $op1$op2 2") + assertThat(failure22).isInstanceOf(SubsequentBinaryOperatorsError::class.java) + assertThat((failure22 as SubsequentBinaryOperatorsError).operator1).isEqualTo(op1) + assertThat(failure22.operator2).isEqualTo(op2) + } + + val failure37 = expectFailureWhenParsingNumericExpression("++2") + assertThat(failure37).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure38 = expectFailureWhenParsingAlgebraicExpression("--x") + assertThat(failure38).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure39 = expectFailureWhenParsingAlgebraicExpression("-+x") + assertThat(failure39).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure40 = expectFailureWhenParsingNumericExpression("+-2") + assertThat(failure40).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + parseNumericExpressionSuccessfully("2++3") // Will succeed since it's 2 + (+2). + val failure41 = expectFailureWhenParsingNumericExpression("2+++3") + assertThat(failure41).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure23 = expectFailureWhenParsingNumericExpression("/2") + assertThat(failure23).isInstanceOf(NoVariableOrNumberBeforeBinaryOperatorError::class.java) + assertThat((failure23 as NoVariableOrNumberBeforeBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.DIVIDE) + + val failure24 = expectFailureWhenParsingAlgebraicExpression("*x") + assertThat(failure24).isInstanceOf(NoVariableOrNumberBeforeBinaryOperatorError::class.java) + assertThat((failure24 as NoVariableOrNumberBeforeBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.MULTIPLY) + + val failure27 = expectFailureWhenParsingNumericExpression("2^") + assertThat(failure27).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure27 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.EXPONENTIATE) + + val failure25 = expectFailureWhenParsingNumericExpression("2/") + assertThat(failure25).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure25 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.DIVIDE) + + val failure26 = expectFailureWhenParsingAlgebraicExpression("x*") + assertThat(failure26).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure26 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.MULTIPLY) + + val failure28 = expectFailureWhenParsingAlgebraicExpression("x+") + assertThat(failure28).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure28 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.ADD) + + val failure29 = expectFailureWhenParsingAlgebraicExpression("x-") + assertThat(failure29).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure29 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.SUBTRACT) + + val failure42 = expectFailureWhenParsingAlgebraicExpression("2^x") + assertThat(failure42).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure43 = expectFailureWhenParsingAlgebraicExpression("2^(1+x)") + assertThat(failure43).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure44 = expectFailureWhenParsingAlgebraicExpression("2^3^x") + assertThat(failure44).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure45 = expectFailureWhenParsingAlgebraicExpression("2^sqrt(x)") + assertThat(failure45).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure46 = expectFailureWhenParsingNumericExpression("2^7") + assertThat(failure46).isInstanceOf(ExponentTooLargeError::class.java) + + val failure47 = expectFailureWhenParsingNumericExpression("2^30.12") + assertThat(failure47).isInstanceOf(ExponentTooLargeError::class.java) + + parseNumericExpressionSuccessfully("2^3") + + val failure48 = expectFailureWhenParsingNumericExpression("2^3^2") + assertThat(failure48).isInstanceOf(NestedExponentsError::class.java) + + val failure49 = expectFailureWhenParsingAlgebraicExpression("x^2^5") + assertThat(failure49).isInstanceOf(NestedExponentsError::class.java) + + val failure20 = expectFailureWhenParsingNumericExpression("2√") + assertThat(failure20).isInstanceOf(HangingSquareRootError::class.java) + + val failure50 = expectFailureWhenParsingNumericExpression("2/0") + assertThat(failure50).isInstanceOf(TermDividedByZeroError::class.java) + + val failure51 = expectFailureWhenParsingAlgebraicExpression("x/0") + assertThat(failure51).isInstanceOf(TermDividedByZeroError::class.java) + + val failure52 = expectFailureWhenParsingNumericExpression("sqrt(2+7/0.0)") + assertThat(failure52).isInstanceOf(TermDividedByZeroError::class.java) + + val failure21 = expectFailureWhenParsingNumericExpression("x+y") + assertThat(failure21).isInstanceOf(VariableInNumericExpressionError::class.java) + + val failure53 = expectFailureWhenParsingAlgebraicExpression("x+y+a") + assertThat(failure53).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure53 as DisabledVariablesInUseError).variables).containsExactly("a") + + val failure54 = expectFailureWhenParsingAlgebraicExpression("apple") + assertThat(failure54).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure54 as DisabledVariablesInUseError).variables) + .containsExactly("a", "p", "l", "e") + + val failure55 = + expectFailureWhenParsingAlgebraicExpression("apple", allowedVariables = listOf("a", "p", "l")) + assertThat(failure55).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure55 as DisabledVariablesInUseError).variables).containsExactly("e") + + parseAlgebraicExpressionSuccessfully("x+y+z") + + val failure56 = + expectFailureWhenParsingAlgebraicExpression("x+y+z", allowedVariables = listOf()) + assertThat(failure56).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure56 as DisabledVariablesInUseError).variables).containsExactly("x", "y", "z") + + val failure30 = expectFailureWhenParsingAlgebraicEquation("x==2") + assertThat(failure30).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + + val failure31 = expectFailureWhenParsingAlgebraicEquation("x=2=y") + assertThat(failure31).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + + val failure32 = expectFailureWhenParsingAlgebraicEquation("x=2=") + assertThat(failure32).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + + val failure33 = expectFailureWhenParsingAlgebraicEquation("x=") + assertThat(failure33).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + + val failure34 = expectFailureWhenParsingAlgebraicEquation("=x") + assertThat(failure34).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + + val failure35 = expectFailureWhenParsingAlgebraicEquation("=x") + assertThat(failure35).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + + // TODO: expand to multiple tests or use parametrized tests. + val prohibitedFunctionNames = + listOf( + "exp", "log", "log10", "ln", "sin", "cos", "tan", "cot", "csc", "sec", "atan", "asin", + "acos", "abs" + ) + for (functionName in prohibitedFunctionNames) { + val failure36 = expectFailureWhenParsingAlgebraicEquation("$functionName(0.5)") + assertThat(failure36).isInstanceOf(InvalidFunctionInUseError::class.java) + assertThat((failure36 as InvalidFunctionInUseError).functionName).isEqualTo(functionName) + } + + val failure57 = expectFailureWhenParsingAlgebraicExpression("sq") + assertThat(failure57).isInstanceOf(FunctionNameIncompleteError::class.java) + + val failure58 = expectFailureWhenParsingAlgebraicExpression("sqr") + assertThat(failure58).isInstanceOf(FunctionNameIncompleteError::class.java) + + // TODO: Other cases: sqrt(, sqrt(), sqrt 2, +2 + } + + private companion object { + // TODO: fix helper API. + + private fun expectFailureWhenParsingNumericExpression(expression: String): MathParsingError { + val result = parseNumericExpressionWithAllErrors(expression) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseNumericExpressionSuccessfully(expression: String): MathExpression { + val result = parseNumericExpressionWithAllErrors(expression) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionWithAllErrors( + expression: String + ): MathParsingResult { + return MathExpressionParser.parseNumericExpression(expression, ErrorCheckingMode.ALL_ERRORS) + } + + private fun expectFailureWhenParsingAlgebraicExpression( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingError { + val result = + parseAlgebraicExpressionWithAllErrors(expression, allowedVariables) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseAlgebraicExpressionSuccessfully( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = parseAlgebraicExpressionWithAllErrors(expression, allowedVariables) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, ErrorCheckingMode.ALL_ERRORS + ) + } + + private fun expectFailureWhenParsingAlgebraicEquation(expression: String): MathParsingError { + val result = parseAlgebraicEquationInternal(expression, ErrorCheckingMode.ALL_ERRORS) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseAlgebraicEquationInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicEquation( + expression, allowedVariables, errorCheckingMode + ) + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt new file mode 100644 index 00000000000..d1f9e17b47d --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -0,0 +1,1777 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class NumericExpressionParserTest { + @Test + fun testLotsOfCasesForNumericExpression() { + // TODO: split this up + // TODO: add log string generation for expressions. + expectFailureWhenParsingNumericExpression("") + + val expression1 = parseNumericExpressionWithAllErrors("1") + assertThat(expression1).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + + expectFailureWhenParsingNumericExpression("x") + + val expression2 = parseNumericExpressionWithAllErrors(" 2 ") + assertThat(expression2).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + + val expression3 = parseNumericExpressionWithAllErrors(" 2.5 ") + assertThat(expression3).hasStructureThatMatches { + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) + } + } + + expectFailureWhenParsingNumericExpression(" x ") + + expectFailureWhenParsingNumericExpression(" z x ") + + val expression4 = parseNumericExpressionWithoutOptionalErrors("2^3^2") + assertThat(expression4).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression23 = parseNumericExpressionWithAllErrors("(2^3)^2") + assertThat(expression23).hasStructureThatMatches { + exponentiation { + leftOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + val expression24 = parseNumericExpressionWithAllErrors("512/32/4") + assertThat(expression24).hasStructureThatMatches { + division { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(512) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(32) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val expression25 = parseNumericExpressionWithAllErrors("512/(32/4)") + assertThat(expression25).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(512) + } + } + rightOperand { + group { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(32) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + val expression5 = parseNumericExpressionWithAllErrors("sqrt(2)") + assertThat(expression5).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + expectFailureWhenParsingNumericExpression("sqr(2)") + + expectFailureWhenParsingNumericExpression("xyz(2)") + + val expression6 = parseNumericExpressionWithAllErrors("732") + assertThat(expression6).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(732) + } + } + + // Verify order of operations between higher & lower precedent operators. + val expression32 = parseNumericExpressionWithAllErrors("3+4^5") + assertThat(expression32).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + + val expression7 = parseNumericExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") + assertThat(expression7).hasStructureThatMatches { + // To better visualize the precedence & order of operations, see this grouped version: + // (((3*2)-3)+((((4^7)*8)/3)*2))+7. + addition { + leftOperand { + // ((3*2)-3)+((((4^7)*8)/3)*2) + addition { + leftOperand { + // (1*2)-3 + subtraction { + leftOperand { + // 3*2 + multiplication { + leftOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // (((4^7)*8)/3)*2 + multiplication { + leftOperand { + // ((4^7)*8)/3 + division { + leftOperand { + // (4^7)*8 + multiplication { + leftOperand { + // 4^7 + exponentiation { + leftOperand { + // 4 + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + rightOperand { + // 8 + constant { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + + expectFailureWhenParsingNumericExpression("x = √2 × 7 ÷ 4") + + val expression8 = parseNumericExpressionWithAllErrors("(1+2)(3+4)") + assertThat(expression8).hasStructureThatMatches { + multiplication { + leftOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsingNumericExpression("(1+2)2") + + val expression10 = parseNumericExpressionWithAllErrors("2(1+2)") + assertThat(expression10).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsingNumericExpression("sqrt(2)3") + + val expression12 = parseNumericExpressionWithAllErrors("3sqrt(2)") + assertThat(expression12).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + expectFailureWhenParsingNumericExpression("xsqrt(2)") + + val expression13 = parseNumericExpressionWithAllErrors("sqrt(2)*(1+2)*(3-2^5)") + assertThat(expression13).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + } + } + } + + val expression58 = parseNumericExpressionWithAllErrors("sqrt(2)(1+2)(3-2^5)") + assertThat(expression58).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + } + } + } + + val expression14 = parseNumericExpressionWithoutOptionalErrors("((3))") + assertThat(expression14).hasStructureThatMatches { + group { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val expression15 = parseNumericExpressionWithoutOptionalErrors("++3") + assertThat(expression15).hasStructureThatMatches { + positive { + operand { + positive { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + + val expression16 = parseNumericExpressionWithoutOptionalErrors("--4") + assertThat(expression16).hasStructureThatMatches { + negation { + operand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression17 = parseNumericExpressionWithAllErrors("1+-4") + assertThat(expression17).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression18 = parseNumericExpressionWithAllErrors("1++4") + assertThat(expression18).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + positive { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression19 = parseNumericExpressionWithAllErrors("1--4") + assertThat(expression19).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + expectFailureWhenParsingNumericExpression("1-^-4") + + val expression20 = parseNumericExpressionWithAllErrors("√2 × 7 ÷ 4") + assertThat(expression20).hasStructureThatMatches { + division { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + expectFailureWhenParsingNumericExpression("1+2 &asdf") + + val expression21 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(3)sqrt(4)") + // Note that this tree demonstrates left associativity. + assertThat(expression21).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression22 = parseNumericExpressionWithAllErrors("(1+2)(3-7^2)(5+-17)") + assertThat(expression22).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + // 1+2 + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + // 3-7^2 + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + rightOperand { + // 5+-17 + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(17) + } + } + } + } + } + } + } + } + } + + val expression26 = parseNumericExpressionWithAllErrors("3^-2") + assertThat(expression26).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression27 = parseNumericExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") + assertThat(expression27).hasStructureThatMatches { + exponentiation { + leftOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + + val expression28 = parseNumericExpressionWithAllErrors("1-3^sqrt(4)") + assertThat(expression28).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + // "Hard" order of operation problems loosely based on & other problems that can often stump + // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. + val expression29 = parseNumericExpressionWithAllErrors("3÷2*(3+4)") + assertThat(expression29).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + val expression59 = parseNumericExpressionWithAllErrors("3÷2(3+4)") + assertThat(expression59).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + // Numbers cannot have implicit multiplication unless they are in groups. + expectFailureWhenParsingNumericExpression("2 2") + + expectFailureWhenParsingNumericExpression("2 2^2") + + expectFailureWhenParsingNumericExpression("2^2 2") + + val expression31 = parseNumericExpressionWithoutOptionalErrors("(3)(4)(5)") + assertThat(expression31).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + + val expression33 = parseNumericExpressionWithoutOptionalErrors("2^(3)") + assertThat(expression33).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + // Verify that implicit multiple has lower precedence than exponentiation. + val expression34 = parseNumericExpressionWithoutOptionalErrors("2^(3)(4)") + assertThat(expression34).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsingNumericExpression("2^(3)2^2") + + val expression35 = parseNumericExpressionWithoutOptionalErrors("2^(3)*2^2") + assertThat(expression35).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + // An exponentiation can be a right operand of an implicit mult if it's grouped. + val expression36 = parseNumericExpressionWithoutOptionalErrors("2^(3)(2^2)") + assertThat(expression36).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsingNumericExpression("2^3(4)2^3") + + val expression38 = parseNumericExpressionWithoutOptionalErrors("2^3(4)*2^3") + assertThat(expression38).hasStructureThatMatches { + // 2^3(4)*2^3 + multiplication { + leftOperand { + // 2^3(4) + multiplication { + leftOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 4 + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + rightOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + + expectFailureWhenParsingNumericExpression("2^2 2^2") + expectFailureWhenParsingNumericExpression("(3) 2^2") + expectFailureWhenParsingNumericExpression("sqrt(3) 2^2") + expectFailureWhenParsingNumericExpression("√2 2^2") + expectFailureWhenParsingNumericExpression("2^2 3") + + expectFailureWhenParsingNumericExpression("-2 3") + + val expression39 = parseNumericExpressionWithAllErrors("-(1+2)") + assertThat(expression39).hasStructureThatMatches { + negation { + operand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + // Should pass for algebra. + expectFailureWhenParsingNumericExpression("-2 x") + + val expression40 = parseNumericExpressionWithAllErrors("-2 (1+2)") + assertThat(expression40).hasStructureThatMatches { + // The negation happens last for parity with other common calculators. + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + + val expression41 = parseNumericExpressionWithoutOptionalErrors("-2^3(4)") + assertThat(expression41).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + val expression43 = parseNumericExpressionWithoutOptionalErrors("√2^2(3)") + assertThat(expression43).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + val expression60 = parseNumericExpressionWithoutOptionalErrors("√(2^2(3))") + assertThat(expression60).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + group { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + } + + val expression42 = parseNumericExpressionWithAllErrors("-2*-2") + // Note that the following structure is not the same as (-2)*(-2) since unary negation has + // higher precedence than multiplication, so it's first & recurses to include the entire + // multiplication expression. + assertThat(expression42).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + + // TODO: Here & elsewhere, fix the fact that this is actually a valid use of single-term + // parentheses (there's a bug in the current error detection logic). + val expression44 = parseNumericExpressionWithoutOptionalErrors("2(2)") + assertThat(expression44).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression45 = parseNumericExpressionWithAllErrors("2sqrt(2)") + assertThat(expression45).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression46 = parseNumericExpressionWithAllErrors("2√2") + assertThat(expression46).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression47 = parseNumericExpressionWithoutOptionalErrors("(2)(2)") + assertThat(expression47).hasStructureThatMatches { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression48 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)(2)") + assertThat(expression48).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression49 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(2)") + assertThat(expression49).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression50 = parseNumericExpressionWithAllErrors("√2√2") + assertThat(expression50).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression51 = parseNumericExpressionWithoutOptionalErrors("(2)(2)(2)") + assertThat(expression51).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression52 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(2)sqrt(2)") + assertThat(expression52).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression53 = parseNumericExpressionWithAllErrors("√2√2√2") + assertThat(expression53).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + // Should fail for algebra. + expectFailureWhenParsingNumericExpression("x7") + + // Should pass for algebra. + expectFailureWhenParsingNumericExpression("2x^2") + + val expression54 = parseNumericExpressionWithAllErrors("2*2/-4+7*2") + assertThat(expression54).hasStructureThatMatches { + // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) + addition { + leftOperand { + // 2*2/-4 + division { + leftOperand { + // 2*2 + multiplication { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + // -4 + negation { + // 4 + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + rightOperand { + // 7*2 + multiplication { + leftOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression55 = parseNumericExpressionWithAllErrors("3/(1-2)") + assertThat(expression55).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + val expression56 = parseNumericExpressionWithoutOptionalErrors("(3)/(1-2)") + assertThat(expression56).hasStructureThatMatches { + division { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + val expression57 = parseNumericExpressionWithoutOptionalErrors("3/((1-2))") + assertThat(expression57).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + group { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + + // TODO: add others, including tests for malformed expressions throughout the parser & + // tokenizer. + } + + private companion object { + // TODO: fix helper API. + + private fun expectFailureWhenParsingNumericExpression(expression: String): MathParsingError { + val result = parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseNumericExpressionWithoutOptionalErrors(expression: String): MathExpression { + val result = + parseNumericExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY + ) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionWithAllErrors(expression: String): MathExpression { + val result = + parseNumericExpressionInternal( + expression, ErrorCheckingMode.ALL_ERRORS + ) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode + ): MathParsingResult { + return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) + } + } +} From 22ae591c2db2d44a5d06b9a3e894df56d0db903e Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 11:27:15 -0800 Subject: [PATCH 109/289] Add exp evaluation & LaTeX conversion support. This is mainly copied from #2173. --- .../testing/math/MathEquationSubject.kt | 11 + .../testing/math/MathExpressionSubject.kt | 30 ++ .../org/oppia/android/util/math/BUILD.bazel | 108 +++++++- .../util/math/ExpressionToLatexConverter.kt | 74 +++++ .../android/util/math/FractionExtensions.kt | 137 +++++++++ .../util/math/MathExpressionExtensions.kt | 13 + .../util/math/NumericExpressionEvaluator.kt | 70 +++++ .../oppia/android/util/math/RealExtensions.kt | 260 ++++++++++++++++++ .../util/math/AlgebraicEquationParserTest.kt | 1 + .../math/AlgebraicExpressionParserTest.kt | 70 +++++ .../org/oppia/android/util/math/BUILD.bazel | 20 ++ .../util/math/ExpressionToLatexTest.kt | 123 +++++++++ .../util/math/NumericExpressionParserTest.kt | 71 +++++ 13 files changed, 977 insertions(+), 11 deletions(-) create mode 100644 utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt index ce24e1e08cc..373b1434b0e 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt @@ -1,10 +1,13 @@ package org.oppia.android.testing.math import com.google.common.truth.FailureMetadata +import com.google.common.truth.StringSubject import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat import com.google.common.truth.extensions.proto.LiteProtoSubject import org.oppia.android.app.model.MathEquation import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.util.math.toRawLatex class MathEquationSubject( metadata: FailureMetadata, @@ -14,6 +17,14 @@ class MathEquationSubject( fun hasRightHandSideThat(): MathExpressionSubject = assertThat(actual.rightSide) + fun convertsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = false)) + + fun convertsWithFractionsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = true)) + + private fun convertToLatex(divAsFraction: Boolean): String = actual.toRawLatex(divAsFraction) + companion object { fun assertThat(actual: MathEquation): MathEquationSubject = assertAbout(::MathEquationSubject).that(actual) diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt index c9be134e209..eea079a7e4b 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt @@ -1,6 +1,8 @@ package org.oppia.android.testing.math +import com.google.common.truth.DoubleSubject import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject import com.google.common.truth.StringSubject import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat @@ -17,6 +19,8 @@ import org.oppia.android.app.model.MathFunctionCall import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.Real import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.oppia.android.util.math.evaluateAsNumericExpression +import org.oppia.android.util.math.toRawLatex // See: https://kotlinlang.org/docs/type-safe-builders.html. class MathExpressionSubject( @@ -28,6 +32,32 @@ class MathExpressionSubject( ExpressionComparator.createFromExpression(actual).also(init) } + fun evaluatesToRationalThat(): FractionSubject = + FractionSubject.assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.RATIONAL).rational) + + fun evaluatesToIrrationalThat(): DoubleSubject = + assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.IRRATIONAL).irrational) + + fun evaluatesToIntegerThat(): IntegerSubject = + assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.INTEGER).integer) + + fun convertsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = false)) + + fun convertsWithFractionsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = true)) + + private fun evaluateAsReal(expectedType: Real.RealTypeCase): Real { + val real = actual.evaluateAsNumericExpression() + assertWithMessage("Failed to evaluate numeric expression").that(real).isNotNull() + assertWithMessage("Expected constant to evaluate to $expectedType") + .that(real?.realTypeCase) + .isEqualTo(expectedType) + return checkNotNull(real) // Just to remove the nullable operator; the actual check is above. + } + + private fun convertToLatex(divAsFraction: Boolean): String = actual.toRawLatex(divAsFraction) + // TODO: update DSL to not have return values (since it's unnecessary). @ExpressionComparatorMarker class ExpressionComparator private constructor(private val expression: MathExpression) { diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 1e0da7381b5..351b04e5f33 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -4,20 +4,18 @@ TODO: document load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") -kt_android_library( +android_library( name = "extensions", - srcs = [ - "FloatExtensions.kt", - "FractionExtensions.kt", - "PolynomialExtensions.kt", - "RatioExtensions.kt", - "RealExtensions.kt", - ], visibility = [ "//:oppia_api_visibility", ], - deps = [ - "//model/src/main/proto:math_java_proto_lite", + exports = [ + ":float_extensions", + ":fraction_extensions", + ":math_expression_extensions", + ":polynomial_extensions", + ":ratio_extensions", + ":real_extensions", ], ) @@ -43,9 +41,9 @@ kt_android_library( "//:oppia_testing_visibility", ], deps = [ - ":extensions", ":parsing_error", ":peekable_iterator", + ":real_extensions", ":tokenizer", "//model/src/main/proto:math_java_proto_lite", ], @@ -71,3 +69,91 @@ kt_android_library( "PeekableIterator.kt", ], ) + +kt_android_library( + name = "float_extensions", + srcs = [ + "FloatExtensions.kt", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "fraction_extensions", + srcs = [ + "FractionExtensions.kt", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "math_expression_extensions", + srcs = [ + "MathExpressionExtensions.kt", + ], + deps = [ + ":expression_to_latex_converter", + ":numeric_expression_evaluator", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "polynomial_extensions", + srcs = [ + "PolynomialExtensions.kt", + ], + deps = [ + ":real_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "ratio_extensions", + srcs = [ + "RatioExtensions.kt", + ], + deps = [ + ":fraction_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "real_extensions", + srcs = [ + "RealExtensions.kt", + ], + deps = [ + ":float_extensions", + ":fraction_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "expression_to_latex_converter", + srcs = [ + "ExpressionToLatexConverter.kt", + ], + deps = [ + ":real_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "numeric_expression_evaluator", + srcs = [ + "NumericExpressionEvaluator.kt", + ], + deps = [ + ":real_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt new file mode 100644 index 00000000000..2c108f02fa7 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt @@ -0,0 +1,74 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall.FunctionType +import org.oppia.android.app.model.MathFunctionCall.FunctionType.FUNCTION_UNSPECIFIED +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator + +class ExpressionToLatexConverter private constructor() { + companion object { + fun MathEquation.convertToLatex(divAsFraction: Boolean): String { + val lhs = leftSide + val rhs = rightSide + return "${lhs.convertToLatex(divAsFraction)} = ${rhs.convertToLatex(divAsFraction)}" + } + + fun MathExpression.convertToLatex(divAsFraction: Boolean): String { + return when (expressionTypeCase) { + CONSTANT -> constant.toPlainText() + VARIABLE -> variable + BINARY_OPERATION -> { + val lhsLatex = binaryOperation.leftOperand.convertToLatex(divAsFraction) + val rhsLatex = binaryOperation.rightOperand.convertToLatex(divAsFraction) + when (binaryOperation.operator) { + ADD -> "$lhsLatex + $rhsLatex" + SUBTRACT -> "$lhsLatex - $rhsLatex" + MULTIPLY -> if (binaryOperation.isImplicit) { + "$lhsLatex$rhsLatex" + } else "$lhsLatex \\times $rhsLatex" + DIVIDE -> if (divAsFraction) { + "\\frac{$lhsLatex}{$rhsLatex}" + } else "$lhsLatex \\div $rhsLatex" + EXPONENTIATE -> "$lhsLatex ^ {$rhsLatex}" + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> + "$lhsLatex $rhsLatex" + } + } + UNARY_OPERATION -> { + val operandLatex = unaryOperation.operand.convertToLatex(divAsFraction) + when (unaryOperation.operator) { + NEGATE -> "-$operandLatex" + POSITIVE -> "+$operandLatex" + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> operandLatex + } + } + FUNCTION_CALL -> { + val argumentLatex = functionCall.argument.convertToLatex(divAsFraction) + when (functionCall.functionType) { + SQUARE_ROOT -> "\\sqrt{$argumentLatex}" + FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> argumentLatex + } + } + GROUP -> "(${group.convertToLatex(divAsFraction)})" + EXPRESSIONTYPE_NOT_SET, null -> "" + } + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index 1da9fef1857..e229fd49e40 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -1,6 +1,7 @@ package org.oppia.android.util.math import org.oppia.android.app.model.Fraction +import kotlin.math.absoluteValue /** Returns whether this fraction has a fractional component. */ fun Fraction.hasFractionalPart(): Boolean { @@ -15,6 +16,12 @@ fun Fraction.isOnlyWholeNumber(): Boolean { return !hasFractionalPart() } +/** + * Returns this fraction as a whole number. Note that this will not return a value that is + * mathematically equivalent to this fraction unless [isOnlyWholeNumber] returns true. + */ +fun Fraction.toWholeNumber(): Int = if (isNegative) -wholeNumber else wholeNumber + /** * Returns a [Double] version of this fraction. * @@ -68,6 +75,22 @@ fun Fraction.toSimplestForm(): Fraction { }.build() } +/** + * Returns this fraction in its proper form by first converting to simplest denominator, then + * extracting a whole number component. + * + * This function will properly convert a fraction whose denominator is 1 into a whole number-only + * fraction. + */ +fun Fraction.toProperForm(): Fraction { + return toSimplestForm().let { + it.toBuilder().apply { + wholeNumber = it.wholeNumber + (it.numerator / it.denominator) + numerator = it.numerator % it.denominator + }.build() + } +} + /** * Returns this fraction in an improper form (that is, with a 0 whole number and only fractional * parts). @@ -80,12 +103,126 @@ fun Fraction.toImproperForm(): Fraction { }.build() } +/** Returns the inverse improper fraction representation of this fraction. */ +fun Fraction.toInvertedImproperForm(): Fraction { + return toImproperForm().let { improper -> + improper.toBuilder().apply { + numerator = improper.denominator + denominator = improper.numerator + }.build() + } +} + /** Returns the negated form of this fraction. */ operator fun Fraction.unaryMinus(): Fraction { return toBuilder().apply { isNegative = !this@unaryMinus.isNegative }.build() } +/** Adds two fractions together and returns a new one in its proper form. */ +operator fun Fraction.plus(rhs: Fraction): Fraction { + // First, eliminate the whole number by computing improper fractions. + val leftFraction = toImproperForm() + val rightFraction = rhs.toImproperForm() + + // Second, find a common denominator and compute the new numerators. + val commonDenominator = lcm(leftFraction.denominator, rightFraction.denominator) + val leftFactor = commonDenominator / leftFraction.denominator + val rightFactor = commonDenominator / rightFraction.denominator + val leftNumerator = leftFraction.numerator * leftFactor + val rightNumerator = rightFraction.numerator * rightFactor + + // Third, determine how the numerators are combined (based on negatives) and whether the result is + // negative. + val leftNeg = leftFraction.isNegative + val rightNeg = rightFraction.isNegative + val (newNumerator, isNegative) = when { + leftNeg && rightNeg -> leftNumerator + rightNumerator to true + !leftNeg && !rightNeg -> leftNumerator + rightNumerator to false + leftNeg && !rightNeg -> + (-leftNumerator + rightNumerator).absoluteValue to (leftNumerator > rightNumerator) + !leftNeg && rightNeg -> + (leftNumerator - rightNumerator).absoluteValue to (rightNumerator > leftNumerator) + else -> throw Exception("Impossible case") + } + + // Finally, compute the new fraction and convert it to proper form to compute its whole number. + return Fraction.newBuilder().apply { + this.isNegative = isNegative + numerator = newNumerator + denominator = commonDenominator + }.build().toProperForm() +} + +/** + * Subtracts the specified fraction from this fraction and returns the result in its proper form. + */ +operator fun Fraction.minus(rhs: Fraction): Fraction { + // a - b = a + -b + return this + -rhs +} + +/** Multiples this fraction by the specified and returns the result in its proper form. */ +operator fun Fraction.times(rhs: Fraction): Fraction { + // First, convert both fractions into their improper forms. + val leftFraction = toImproperForm() + val rightFraction = rhs.toImproperForm() + + // Second, multiple the numerators and denominators piece-wise. + val newNumerator = leftFraction.numerator * rightFraction.numerator + val newDenominator = leftFraction.denominator * rightFraction.denominator + + // Third, determine negative (negative is retained if only one is negative). + val isNegative = leftFraction.isNegative xor rightFraction.isNegative + return Fraction.newBuilder().apply { + this.isNegative = isNegative + numerator = newNumerator + denominator = newDenominator + }.build().toProperForm() +} + +/** Returns the proper form of the division from this fraction by the specified fraction. */ +operator fun Fraction.div(rhs: Fraction): Fraction { + // a / b = a * b^-1 (b's inverse). + return this * rhs.toInvertedImproperForm() +} + +fun Fraction.pow(exp: Int): Fraction { + return when { + exp == 0 -> { + Fraction.newBuilder().apply { + wholeNumber = 1 + denominator = 1 + }.build() + } + exp == 1 -> this + // x^-2 == 1/(x^2). + exp < 1 -> pow(-exp).toInvertedImproperForm().toProperForm() + else -> { // i > 1 + var newValue = this + for (i in 1 until exp) newValue *= this + return newValue + } + } +} + +/** Returns the [Fraction] representation of this integer (as a whole number fraction). */ +fun Int.toWholeNumberFraction(): Fraction { + val intValue = this + return Fraction.newBuilder().apply { + isNegative = intValue < 0 + wholeNumber = kotlin.math.abs(intValue) + numerator = 0 + denominator = 1 + }.build() +} + /** Returns the greatest common divisor between two integers. */ fun gcd(x: Int, y: Int): Int { return if (y == 0) x else gcd(y, x % y) } + +/** Returns the least common multiple between two integers. */ +private fun lcm(x: Int, y: Int): Int { + // Reference: https://en.wikipedia.org/wiki/Least_common_multiple#Calculation. + return (x * y).absoluteValue / gcd(x, y) +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt new file mode 100644 index 00000000000..59b203a0d2d --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -0,0 +1,13 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.Real +import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex +import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate + +fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) + +fun MathExpression.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) + +fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt new file mode 100644 index 00000000000..766da590400 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt @@ -0,0 +1,70 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathFunctionCall.FunctionType +import org.oppia.android.app.model.MathFunctionCall.FunctionType.FUNCTION_UNSPECIFIED +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.Real +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator + +class NumericExpressionEvaluator private constructor() { + companion object { + fun MathExpression.evaluate(): Real? { + return when (expressionTypeCase) { + CONSTANT -> constant + VARIABLE -> null // Variables not supported in numeric expressions. + BINARY_OPERATION -> binaryOperation.evaluate() + UNARY_OPERATION -> unaryOperation.evaluate() + FUNCTION_CALL -> functionCall.evaluate() + GROUP -> group.evaluate() + EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathBinaryOperation.evaluate(): Real? { + return when (operator) { + ADD -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.plus(it) } + SUBTRACT -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.minus(it) } + MULTIPLY -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.times(it) } + DIVIDE -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.div(it) } + EXPONENTIATE -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.pow(it) } + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null + } + } + + private fun MathUnaryOperation.evaluate(): Real? { + return when (operator) { + NEGATE -> operand.evaluate()?.let { -it } + POSITIVE -> operand.evaluate() // '+2' is the same as just '2'. + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> null + } + } + + private fun MathFunctionCall.evaluate(): Real? { + return when (functionType) { + SQUARE_ROOT -> argument.evaluate()?.let { sqrt(it) } + FUNCTION_UNSPECIFIED, + FunctionType.UNRECOGNIZED, + null -> null + } + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 6df36abd3b6..83605b05808 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -1,13 +1,17 @@ package org.oppia.android.util.math +import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.Real import org.oppia.android.app.model.Real.RealTypeCase.INTEGER import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET +import kotlin.math.pow fun Real.isRational(): Boolean = realTypeCase == RATIONAL +fun Real.isInteger(): Boolean = realTypeCase == INTEGER + fun Real.isNegative(): Boolean = when (realTypeCase) { RATIONAL -> rational.isNegative IRRATIONAL -> irrational < 0 @@ -46,8 +50,264 @@ operator fun Real.unaryMinus(): Real { } } +operator fun Real.plus(rhs: Real): Real { + return combine( + this, rhs, Fraction::plus, Fraction::plus, Fraction::plus, Double::plus, Double::plus, + Double::plus, Int::plus, Int::plus, Int::add + ) +} + +operator fun Real.minus(rhs: Real): Real { + return combine( + this, rhs, Fraction::minus, Fraction::minus, Fraction::minus, Double::minus, Double::minus, + Double::minus, Int::minus, Int::minus, Int::subtract + ) +} + +operator fun Real.times(rhs: Real): Real { + return combine( + this, rhs, Fraction::times, Fraction::times, Fraction::times, Double::times, Double::times, + Double::times, Int::times, Int::times, Int::multiply + ) +} + +operator fun Real.div(rhs: Real): Real { + return combine( + this, rhs, Fraction::div, Fraction::div, Fraction::div, Double::div, Double::div, Double::div, + Int::div, Int::div, Int::divide + ) +} + +fun Real.pow(rhs: Real): Real { + // Powers can really only be effectively done via floats or whole-number only fractions. + return when (realTypeCase) { + RATIONAL -> { + // Left-hand side is Fraction. + when (rhs.realTypeCase) { + RATIONAL -> recompute { + if (rhs.rational.isOnlyWholeNumber()) { + // The fraction can be retained. + it.setRational(rational.pow(rhs.rational.wholeNumber)) + } else { + // The fraction can't realistically be retained since it's being raised to an actual + // fraction, resulting in an irrational number. + it.setIrrational(rational.toDouble().pow(rhs.rational.toDouble())) + } + } + IRRATIONAL -> recompute { it.setIrrational(rational.pow(rhs.irrational)) } + INTEGER -> recompute { it.setRational(rational.pow(rhs.integer)) } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + IRRATIONAL -> { + // Left-hand side is a double. + when (rhs.realTypeCase) { + RATIONAL -> recompute { it.setIrrational(irrational.pow(rhs.rational)) } + IRRATIONAL -> recompute { it.setIrrational(irrational.pow(rhs.irrational)) } + INTEGER -> recompute { it.setIrrational(irrational.pow(rhs.integer)) } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + INTEGER -> { + // Left-hand side is an integer. + when (rhs.realTypeCase) { + RATIONAL -> { + if (rhs.rational.isOnlyWholeNumber()) { + // Whole number-only fractions are effectively just int^int. + integer.pow(rhs.rational.wholeNumber) + } else { + // Otherwise, raising by a fraction will result in an irrational number. + recompute { it.setIrrational(integer.toDouble().pow(rhs.rational.toDouble())) } + } + } + IRRATIONAL -> recompute { it.setIrrational(integer.toDouble().pow(rhs.irrational)) } + INTEGER -> integer.pow(rhs.integer) + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + } +} + +fun sqrt(real: Real): Real { + return when (real.realTypeCase) { + RATIONAL -> sqrt(real.rational) + IRRATIONAL -> real.recompute { it.setIrrational(kotlin.math.sqrt(real.irrational)) } + INTEGER -> sqrt(real.integer) + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $real.") + } +} + fun abs(real: Real): Real = if (real.isNegative()) -real else real +private operator fun Double.plus(rhs: Fraction): Double = this + rhs.toDouble() +private operator fun Fraction.plus(rhs: Double): Double = toDouble() + rhs +private operator fun Fraction.plus(rhs: Int): Fraction = this + rhs.toWholeNumberFraction() +private operator fun Int.plus(rhs: Fraction): Fraction = toWholeNumberFraction() + rhs +private operator fun Double.minus(rhs: Fraction): Double = this - rhs.toDouble() +private operator fun Fraction.minus(rhs: Double): Double = toDouble() - rhs +private operator fun Fraction.minus(rhs: Int): Fraction = this - rhs.toWholeNumberFraction() +private operator fun Int.minus(rhs: Fraction): Fraction = toWholeNumberFraction() - rhs +private operator fun Double.times(rhs: Fraction): Double = this * rhs.toDouble() +private operator fun Fraction.times(rhs: Double): Double = toDouble() * rhs +private operator fun Fraction.times(rhs: Int): Fraction = this * rhs.toWholeNumberFraction() +private operator fun Int.times(rhs: Fraction): Fraction = toWholeNumberFraction() * rhs +private operator fun Double.div(rhs: Fraction): Double = this / rhs.toDouble() +private operator fun Fraction.div(rhs: Double): Double = toDouble() / rhs +private operator fun Fraction.div(rhs: Int): Fraction = this / rhs.toWholeNumberFraction() +private operator fun Int.div(rhs: Fraction): Fraction = toWholeNumberFraction() / rhs + +private fun Int.add(rhs: Int): Real = Real.newBuilder().apply { integer = this@add + rhs }.build() +private fun Int.subtract(rhs: Int): Real = Real.newBuilder().apply { + integer = this@subtract - rhs +}.build() +private fun Int.multiply(rhs: Int): Real = Real.newBuilder().apply { + integer = this@multiply * rhs +}.build() +private fun Int.divide(rhs: Int): Real = Real.newBuilder().apply { + // If rhs divides this integer, retain the integer. + val lhs = this@divide + if ((lhs % rhs) == 0) { + integer = lhs / rhs + } else { + // Otherwise, keep precision by turning the division into a fraction. + rational = Fraction.newBuilder().apply { + isNegative = (lhs < 0) xor (rhs < 0) + numerator = kotlin.math.abs(lhs) + denominator = kotlin.math.abs(rhs) + }.build() + } +}.build() + +private fun Double.pow(rhs: Fraction): Double = this.pow(rhs.toDouble()) +private fun Fraction.pow(rhs: Double): Double = toDouble().pow(rhs) + +private fun Int.pow(exp: Int): Real { + return when { + exp == 0 -> Real.newBuilder().apply { integer = 0 }.build() + exp == 1 -> Real.newBuilder().apply { integer = this@pow }.build() + exp < 0 -> Real.newBuilder().apply { rational = toWholeNumberFraction().pow(exp) }.build() + else -> { + // exp > 1 + var computed = this + for (i in 0 until exp - 1) computed *= this + Real.newBuilder().apply { integer = computed }.build() + } + } +} + +private fun sqrt(fraction: Fraction): Real { + val improper = fraction.toImproperForm() + + // Attempt to take the root of the fraction's numerator & denominator. + val numeratorRoot = sqrt(improper.numerator) + val denominatorRoot = sqrt(improper.denominator) + + // If both values stayed as integers, the original fraction can be retained. Otherwise, the + // fraction must be evaluated by performing a division. + return Real.newBuilder().apply { + if (numeratorRoot.realTypeCase == denominatorRoot.realTypeCase && numeratorRoot.isInteger()) { + val rootedFraction = Fraction.newBuilder().apply { + isNegative = improper.isNegative + numerator = numeratorRoot.integer + denominator = denominatorRoot.integer + }.build().toProperForm() + if (rootedFraction.isOnlyWholeNumber()) { + // If the fractional form doesn't need to be kept, remove it. + integer = rootedFraction.toWholeNumber() + } else { + rational = rootedFraction + } + } else { + irrational = numeratorRoot.toDouble() + } + }.build() +} + +private fun sqrt(int: Int): Real { + // First, check if the integer is a square. Reference for possible methods: + // https://www.researchgate.net/post/How-to-decide-if-a-given-number-will-have-integer-square-root-or-not. + var potentialRoot = 2 + while ((potentialRoot * potentialRoot) < int) { + potentialRoot++ + } + if (potentialRoot * potentialRoot == int) { + // There's an exact integer representation of the root. + return Real.newBuilder().apply { + integer = potentialRoot + }.build() + } + + // Otherwise, compute the irrational square root. + return Real.newBuilder().apply { + irrational = kotlin.math.sqrt(int.toDouble()) + }.build() +} + private fun Real.recompute(transform: (Real.Builder) -> Real.Builder): Real { return transform(newBuilderForType()).build() } + +// TODO: consider replacing this with inline alternatives since they'll probably be simpler. +private fun combine( + lhs: Real, + rhs: Real, + leftRationalRightRationalOp: (Fraction, Fraction) -> Fraction, + leftRationalRightIrrationalOp: (Fraction, Double) -> Double, + leftRationalRightIntegerOp: (Fraction, Int) -> Fraction, + leftIrrationalRightRationalOp: (Double, Fraction) -> Double, + leftIrrationalRightIrrationalOp: (Double, Double) -> Double, + leftIrrationalRightIntegerOp: (Double, Int) -> Double, + leftIntegerRightRationalOp: (Int, Fraction) -> Fraction, + leftIntegerRightIrrationalOp: (Int, Double) -> Double, + leftIntegerRightIntegerOp: (Int, Int) -> Real, +): Real { + return when (lhs.realTypeCase) { + RATIONAL -> { + // Left-hand side is Fraction. + when (rhs.realTypeCase) { + RATIONAL -> + lhs.recompute { it.setRational(leftRationalRightRationalOp(lhs.rational, rhs.rational)) } + IRRATIONAL -> + lhs.recompute { + it.setIrrational(leftRationalRightIrrationalOp(lhs.rational, rhs.irrational)) + } + INTEGER -> + lhs.recompute { it.setRational(leftRationalRightIntegerOp(lhs.rational, rhs.integer)) } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + IRRATIONAL -> { + // Left-hand side is a double. + when (rhs.realTypeCase) { + RATIONAL -> + lhs.recompute { + it.setIrrational(leftIrrationalRightRationalOp(lhs.irrational, rhs.rational)) + } + IRRATIONAL -> + lhs.recompute { + it.setIrrational(leftIrrationalRightIrrationalOp(lhs.irrational, rhs.irrational)) + } + INTEGER -> + lhs.recompute { + it.setIrrational(leftIrrationalRightIntegerOp(lhs.irrational, rhs.integer)) + } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + INTEGER -> { + // Left-hand side is an integer. + when (rhs.realTypeCase) { + RATIONAL -> + lhs.recompute { it.setRational(leftIntegerRightRationalOp(lhs.integer, rhs.rational)) } + IRRATIONAL -> + lhs.recompute { + it.setIrrational(leftIntegerRightIrrationalOp(lhs.integer, rhs.irrational)) + } + INTEGER -> leftIntegerRightIntegerOp(lhs.integer, rhs.integer) + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $lhs.") + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt index 94fc6e50ab4..a2cb45387d9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt @@ -25,6 +25,7 @@ class AlgebraicEquationParserTest { withNameThat().isEqualTo("x") } } + assertThat(equation1).hasRightHandSideThat().evaluatesToIntegerThat().isEqualTo(1) val equation2 = parseAlgebraicEquationSuccessfully( diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt index 9fea4084970..62c88f449a6 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt @@ -10,6 +10,7 @@ import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode +import kotlin.math.sqrt /** Tests for [MathExpressionParser]. */ @RunWith(AndroidJUnit4::class) @@ -27,6 +28,7 @@ class AlgebraicExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(1) } } + assertThat(expression1).evaluatesToIntegerThat().isEqualTo(1) val expression61 = parseAlgebraicExpressionWithAllErrors("x") assertThat(expression61).hasStructureThatMatches { @@ -41,6 +43,7 @@ class AlgebraicExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(2) } } + assertThat(expression2).evaluatesToIntegerThat().isEqualTo(2) val expression3 = parseAlgebraicExpressionWithAllErrors(" 2.5 ") assertThat(expression3).hasStructureThatMatches { @@ -48,6 +51,7 @@ class AlgebraicExpressionParserTest { withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) } } + assertThat(expression3).evaluatesToIrrationalThat().isWithin(1e-5).of(2.5) val expression62 = parseAlgebraicExpressionWithAllErrors(" y ") assertThat(expression62).hasStructureThatMatches { @@ -96,6 +100,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression4).evaluatesToIntegerThat().isEqualTo(512) val expression23 = parseAlgebraicExpressionWithAllErrors("(2^3)^2") assertThat(expression23).hasStructureThatMatches { @@ -123,6 +128,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression23).evaluatesToIntegerThat().isEqualTo(64) val expression24 = parseAlgebraicExpressionWithAllErrors("512/32/4") assertThat(expression24).hasStructureThatMatches { @@ -148,6 +154,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression24).evaluatesToIntegerThat().isEqualTo(4) val expression25 = parseAlgebraicExpressionWithAllErrors("512/(32/4)") assertThat(expression25).hasStructureThatMatches { @@ -175,6 +182,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression25).evaluatesToIntegerThat().isEqualTo(64) val expression5 = parseAlgebraicExpressionWithAllErrors("sqrt(2)") assertThat(expression5).hasStructureThatMatches { @@ -186,6 +194,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression5).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0)) expectFailureWhenParsingAlgebraicExpression("sqr(2)") @@ -231,6 +240,7 @@ class AlgebraicExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(732) } } + assertThat(expression6).evaluatesToIntegerThat().isEqualTo(732) expectFailureWhenParsingAlgebraicExpression("73 2") @@ -259,6 +269,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression32).evaluatesToIntegerThat().isEqualTo(1027) val expression7 = parseAlgebraicExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") assertThat(expression7).hasStructureThatMatches { @@ -356,6 +367,11 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression7) + .evaluatesToRationalThat() + .evaluatesToRealThat() + .isWithin(1e-5) + .of(87391.333333333) expectFailureWhenParsingAlgebraicExpression("x = √2 × 7 ÷ 4") @@ -396,6 +412,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression8).evaluatesToIntegerThat().isEqualTo(21) // Right implicit multiplication of numbers isn't allowed. expectFailureWhenParsingAlgebraicExpression("(1+2)2") @@ -426,6 +443,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression10).evaluatesToIntegerThat().isEqualTo(6) // Right implicit multiplication of numbers isn't allowed. expectFailureWhenParsingAlgebraicExpression("sqrt(2)3") @@ -449,6 +467,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression12).evaluatesToIrrationalThat().isWithin(1e-5).of(3.0 * sqrt(2.0)) val expression65 = parseAlgebraicExpressionWithAllErrors("xsqrt(2)") assertThat(expression65).hasStructureThatMatches { @@ -529,6 +548,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression13).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) val expression58 = parseAlgebraicExpressionWithAllErrors("sqrt(2)(1+2)(3-2^5)") assertThat(expression58).hasStructureThatMatches { @@ -589,6 +609,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression58).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) val expression14 = parseAlgebraicExpressionWithoutOptionalErrors("((3))") assertThat(expression14).hasStructureThatMatches { @@ -600,6 +621,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression14).evaluatesToIntegerThat().isEqualTo(3) val expression15 = parseAlgebraicExpressionWithoutOptionalErrors("++3") assertThat(expression15).hasStructureThatMatches { @@ -615,6 +637,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression15).evaluatesToIntegerThat().isEqualTo(3) val expression16 = parseAlgebraicExpressionWithoutOptionalErrors("--4") assertThat(expression16).hasStructureThatMatches { @@ -630,6 +653,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression16).evaluatesToIntegerThat().isEqualTo(4) val expression17 = parseAlgebraicExpressionWithAllErrors("1+-4") assertThat(expression17).hasStructureThatMatches { @@ -650,6 +674,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression17).evaluatesToIntegerThat().isEqualTo(-3) val expression18 = parseAlgebraicExpressionWithAllErrors("1++4") assertThat(expression18).hasStructureThatMatches { @@ -670,6 +695,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression18).evaluatesToIntegerThat().isEqualTo(5) val expression19 = parseAlgebraicExpressionWithAllErrors("1--4") assertThat(expression19).hasStructureThatMatches { @@ -690,6 +716,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression19).evaluatesToIntegerThat().isEqualTo(5) expectFailureWhenParsingAlgebraicExpression("1-^-4") @@ -721,6 +748,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression20).evaluatesToIrrationalThat().isWithin(1e-5).of((sqrt(2.0) * 7.0) / 4.0) expectFailureWhenParsingAlgebraicExpression("1+2 &asdf") @@ -761,6 +789,10 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression21) + .evaluatesToIrrationalThat() + .isWithin(1e-5) + .of(sqrt(2.0) * sqrt(3.0) * sqrt(4.0)) val expression22 = parseAlgebraicExpressionWithAllErrors("(1+2)(3-7^2)(5+-17)") assertThat(expression22).hasStructureThatMatches { @@ -835,6 +867,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression22).evaluatesToIntegerThat().isEqualTo(1656) val expression26 = parseAlgebraicExpressionWithAllErrors("3^-2") assertThat(expression26).hasStructureThatMatches { @@ -855,6 +888,12 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression26).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(9) + } val expression27 = parseAlgebraicExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") assertThat(expression27).hasStructureThatMatches { @@ -901,6 +940,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression27).evaluatesToIrrationalThat().isWithin(1e-5).of(0.78338103693) val expression28 = parseAlgebraicExpressionWithAllErrors("1-3^sqrt(4)") assertThat(expression28).hasStructureThatMatches { @@ -930,6 +970,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression28).evaluatesToIntegerThat().isEqualTo(-8) // "Hard" order of operation problems loosely based on & other problems that can often stump // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. @@ -968,6 +1009,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression29).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) val expression59 = parseAlgebraicExpressionWithAllErrors("3÷2(3+4)") assertThat(expression59).hasStructureThatMatches { @@ -1004,6 +1046,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression59).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) // Numbers cannot have implicit multiplication unless they are in groups. expectFailureWhenParsingAlgebraicExpression("2 2") @@ -1042,6 +1085,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression31).evaluatesToIntegerThat().isEqualTo(60) val expression33 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)") assertThat(expression33).hasStructureThatMatches { @@ -1060,6 +1104,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression33).evaluatesToIntegerThat().isEqualTo(8) // Verify that implicit multiple has lower precedence than exponentiation. val expression34 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(4)") @@ -1090,6 +1135,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression34).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can never be an implicit right operand. expectFailureWhenParsingAlgebraicExpression("2^(3)2^2") @@ -1129,6 +1175,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression35).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can be a right operand of an implicit mult if it's grouped. val expression36 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(2^2)") @@ -1168,6 +1215,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression36).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can never be an implicit right operand. expectFailureWhenParsingAlgebraicExpression("2^3(4)2^3") @@ -1225,6 +1273,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression38).evaluatesToIntegerThat().isEqualTo(256) expectFailureWhenParsingAlgebraicExpression("2^2 2^2") expectFailureWhenParsingAlgebraicExpression("(3) 2^2") @@ -1255,6 +1304,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression39).evaluatesToIntegerThat().isEqualTo(-3) // Should pass for algebra. val expression66 = parseAlgebraicExpressionWithAllErrors("-2 x") @@ -1308,6 +1358,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression40).evaluatesToIntegerThat().isEqualTo(-6) val expression41 = parseAlgebraicExpressionWithoutOptionalErrors("-2^3(4)") assertThat(expression41).hasStructureThatMatches { @@ -1339,6 +1390,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression41).evaluatesToIntegerThat().isEqualTo(-32) val expression43 = parseAlgebraicExpressionWithoutOptionalErrors("√2^2(3)") assertThat(expression43).hasStructureThatMatches { @@ -1370,6 +1422,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression43).evaluatesToIrrationalThat().isWithin(1e-5).of(6.0) val expression60 = parseAlgebraicExpressionWithoutOptionalErrors("√(2^2(3))") assertThat(expression60).hasStructureThatMatches { @@ -1403,6 +1456,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression60).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(12.0)) val expression42 = parseAlgebraicExpressionWithAllErrors("-2*-2") // Note that the following structure is not the same as (-2)*(-2) since unary negation has @@ -1430,6 +1484,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression42).evaluatesToIntegerThat().isEqualTo(4) val expression44 = parseAlgebraicExpressionWithoutOptionalErrors("2(2)") assertThat(expression44).hasStructureThatMatches { @@ -1448,6 +1503,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression44).evaluatesToIntegerThat().isEqualTo(4) val expression45 = parseAlgebraicExpressionWithAllErrors("2sqrt(2)") assertThat(expression45).hasStructureThatMatches { @@ -1468,6 +1524,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression45).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) val expression46 = parseAlgebraicExpressionWithAllErrors("2√2") assertThat(expression46).hasStructureThatMatches { @@ -1488,6 +1545,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression46).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) val expression47 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)") assertThat(expression47).hasStructureThatMatches { @@ -1508,6 +1566,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression47).evaluatesToIntegerThat().isEqualTo(4) val expression48 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)(2)") assertThat(expression48).hasStructureThatMatches { @@ -1530,6 +1589,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression48).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) val expression49 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(2)") assertThat(expression49).hasStructureThatMatches { @@ -1554,6 +1614,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression49).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) val expression50 = parseAlgebraicExpressionWithAllErrors("√2√2") assertThat(expression50).hasStructureThatMatches { @@ -1578,6 +1639,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression50).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) val expression51 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)(2)") assertThat(expression51).hasStructureThatMatches { @@ -1609,6 +1671,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression51).evaluatesToIntegerThat().isEqualTo(8) val expression52 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(2)sqrt(2)") assertThat(expression52).hasStructureThatMatches { @@ -1646,6 +1709,8 @@ class AlgebraicExpressionParserTest { } } } + val sqrt2 = sqrt(2.0) + assertThat(expression52).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) val expression53 = parseAlgebraicExpressionWithAllErrors("√2√2√2") assertThat(expression53).hasStructureThatMatches { @@ -1683,6 +1748,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression53).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) // Should fail for algebra. expectFailureWhenParsingAlgebraicExpression("x7") @@ -1801,6 +1867,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression54).evaluatesToIntegerThat().isEqualTo(13) val expression55 = parseAlgebraicExpressionWithAllErrors("3/(1-2)") assertThat(expression55).hasStructureThatMatches { @@ -1828,6 +1895,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression55).evaluatesToIntegerThat().isEqualTo(-3) val expression56 = parseAlgebraicExpressionWithoutOptionalErrors("(3)/(1-2)") assertThat(expression56).hasStructureThatMatches { @@ -1857,6 +1925,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression56).evaluatesToIntegerThat().isEqualTo(-3) val expression57 = parseAlgebraicExpressionWithoutOptionalErrors("3/((1-2))") assertThat(expression57).hasStructureThatMatches { @@ -1886,6 +1955,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression57).evaluatesToIntegerThat().isEqualTo(-3) // TODO: add others, including tests for malformed expressions throughout the parser & // tokenizer. diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 2a8b0c295c4..282038acd19 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -49,6 +49,26 @@ oppia_android_test( ], ) +oppia_android_test( + name = "ExpressionToLatexTest", + srcs = ["ExpressionToLatexTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.ExpressionToLatexTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", + "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:parser", + ], +) + oppia_android_test( name = "MathExpressionParserTest", srcs = ["MathExpressionParserTest.kt"], diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt new file mode 100644 index 00000000000..e56db9a7500 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt @@ -0,0 +1,123 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ExpressionToLatexTest { + @Test + fun testLatex() { + // TODO: split up & move to separate test suites. Finish test cases. + + val exp1 = parseNumericExpressionWithAllErrors("1") + assertThat(exp1).convertsToLatexStringThat().isEqualTo("1") + + val exp2 = parseNumericExpressionWithAllErrors("1+2") + assertThat(exp2).convertsToLatexStringThat().isEqualTo("1 + 2") + + val exp3 = parseNumericExpressionWithAllErrors("1*2") + assertThat(exp3).convertsToLatexStringThat().isEqualTo("1 \\times 2") + + val exp4 = parseNumericExpressionWithAllErrors("1/2") + assertThat(exp4).convertsToLatexStringThat().isEqualTo("1 \\div 2") + + val exp5 = parseNumericExpressionWithAllErrors("1/2") + assertThat(exp5).convertsWithFractionsToLatexStringThat().isEqualTo("\\frac{1}{2}") + + val exp10 = parseNumericExpressionWithAllErrors("√2") + assertThat(exp10).convertsToLatexStringThat().isEqualTo("\\sqrt{2}") + + val exp11 = parseNumericExpressionWithAllErrors("√(1/2)") + assertThat(exp11).convertsToLatexStringThat().isEqualTo("\\sqrt{(1 \\div 2)}") + + val exp6 = parseAlgebraicExpressionWithAllErrors("x+y") + assertThat(exp6).convertsToLatexStringThat().isEqualTo("x + y") + + val exp7 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1/y)") + assertThat(exp7).convertsToLatexStringThat().isEqualTo("x ^ {(1 \\div y)}") + + val exp8 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1/y)") + assertThat(exp8).convertsWithFractionsToLatexStringThat().isEqualTo("x ^ {(\\frac{1}{y})}") + + val exp9 = parseAlgebraicExpressionWithoutOptionalErrors("x^y^z") + assertThat(exp9).convertsWithFractionsToLatexStringThat().isEqualTo("x ^ {y ^ {z}}") + + val eq1 = + parseAlgebraicEquationWithAllErrors( + "7a^2+b^2+c^2=0", allowedVariables = listOf("a", "b", "c") + ) + assertThat(eq1).convertsToLatexStringThat().isEqualTo("7a ^ {2} + b ^ {2} + c ^ {2} = 0") + + val eq2 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") + assertThat(eq2).convertsToLatexStringThat().isEqualTo("\\sqrt{1 + x} \\div x = 1") + + val eq3 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") + assertThat(eq3) + .convertsWithFractionsToLatexStringThat() + .isEqualTo("\\frac{\\sqrt{1 + x}}{x} = 1") + } + + private companion object { + // TODO: fix helper API. + + private fun parseNumericExpressionWithAllErrors(expression: String): MathExpression { + val result = + MathExpressionParser.parseNumericExpression(expression, ErrorCheckingMode.ALL_ERRORS) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionWithoutOptionalErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) + } + + private fun parseAlgebraicEquationWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathEquation { + val result = + MathExpressionParser.parseAlgebraicEquation( + expression, allowedVariables, + ErrorCheckingMode.ALL_ERRORS + ) + return (result as MathParsingResult.Success).result + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index d1f9e17b47d..2dacceff76e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -10,11 +10,13 @@ import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode +import kotlin.math.sqrt /** Tests for [MathExpressionParser]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class NumericExpressionParserTest { + @Test fun testLotsOfCasesForNumericExpression() { // TODO: split this up @@ -27,6 +29,7 @@ class NumericExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(1) } } + assertThat(expression1).evaluatesToIntegerThat().isEqualTo(1) expectFailureWhenParsingNumericExpression("x") @@ -36,6 +39,7 @@ class NumericExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(2) } } + assertThat(expression2).evaluatesToIntegerThat().isEqualTo(2) val expression3 = parseNumericExpressionWithAllErrors(" 2.5 ") assertThat(expression3).hasStructureThatMatches { @@ -43,6 +47,7 @@ class NumericExpressionParserTest { withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) } } + assertThat(expression3).evaluatesToIrrationalThat().isWithin(1e-5).of(2.5) expectFailureWhenParsingNumericExpression(" x ") @@ -72,6 +77,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression4).evaluatesToIntegerThat().isEqualTo(512) val expression23 = parseNumericExpressionWithAllErrors("(2^3)^2") assertThat(expression23).hasStructureThatMatches { @@ -99,6 +105,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression23).evaluatesToIntegerThat().isEqualTo(64) val expression24 = parseNumericExpressionWithAllErrors("512/32/4") assertThat(expression24).hasStructureThatMatches { @@ -124,6 +131,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression24).evaluatesToIntegerThat().isEqualTo(4) val expression25 = parseNumericExpressionWithAllErrors("512/(32/4)") assertThat(expression25).hasStructureThatMatches { @@ -151,6 +159,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression25).evaluatesToIntegerThat().isEqualTo(64) val expression5 = parseNumericExpressionWithAllErrors("sqrt(2)") assertThat(expression5).hasStructureThatMatches { @@ -162,6 +171,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression5).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0)) expectFailureWhenParsingNumericExpression("sqr(2)") @@ -173,6 +183,7 @@ class NumericExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(732) } } + assertThat(expression6).evaluatesToIntegerThat().isEqualTo(732) // Verify order of operations between higher & lower precedent operators. val expression32 = parseNumericExpressionWithAllErrors("3+4^5") @@ -199,6 +210,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression32).evaluatesToIntegerThat().isEqualTo(1027) val expression7 = parseNumericExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") assertThat(expression7).hasStructureThatMatches { @@ -296,6 +308,11 @@ class NumericExpressionParserTest { } } } + assertThat(expression7) + .evaluatesToRationalThat() + .evaluatesToRealThat() + .isWithin(1e-5) + .of(87391.333333333) expectFailureWhenParsingNumericExpression("x = √2 × 7 ÷ 4") @@ -336,6 +353,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression8).evaluatesToIntegerThat().isEqualTo(21) // Right implicit multiplication of numbers isn't allowed. expectFailureWhenParsingNumericExpression("(1+2)2") @@ -366,6 +384,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression10).evaluatesToIntegerThat().isEqualTo(6) // Right implicit multiplication of numbers isn't allowed. expectFailureWhenParsingNumericExpression("sqrt(2)3") @@ -389,6 +408,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression12).evaluatesToIrrationalThat().isWithin(1e-5).of(3.0 * sqrt(2.0)) expectFailureWhenParsingNumericExpression("xsqrt(2)") @@ -451,6 +471,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression13).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) val expression58 = parseNumericExpressionWithAllErrors("sqrt(2)(1+2)(3-2^5)") assertThat(expression58).hasStructureThatMatches { @@ -511,6 +532,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression58).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) val expression14 = parseNumericExpressionWithoutOptionalErrors("((3))") assertThat(expression14).hasStructureThatMatches { @@ -522,6 +544,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression14).evaluatesToIntegerThat().isEqualTo(3) val expression15 = parseNumericExpressionWithoutOptionalErrors("++3") assertThat(expression15).hasStructureThatMatches { @@ -537,6 +560,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression15).evaluatesToIntegerThat().isEqualTo(3) val expression16 = parseNumericExpressionWithoutOptionalErrors("--4") assertThat(expression16).hasStructureThatMatches { @@ -552,6 +576,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression16).evaluatesToIntegerThat().isEqualTo(4) val expression17 = parseNumericExpressionWithAllErrors("1+-4") assertThat(expression17).hasStructureThatMatches { @@ -572,6 +597,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression17).evaluatesToIntegerThat().isEqualTo(-3) val expression18 = parseNumericExpressionWithAllErrors("1++4") assertThat(expression18).hasStructureThatMatches { @@ -592,6 +618,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression18).evaluatesToIntegerThat().isEqualTo(5) val expression19 = parseNumericExpressionWithAllErrors("1--4") assertThat(expression19).hasStructureThatMatches { @@ -612,6 +639,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression19).evaluatesToIntegerThat().isEqualTo(5) expectFailureWhenParsingNumericExpression("1-^-4") @@ -643,6 +671,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression20).evaluatesToIrrationalThat().isWithin(1e-5).of((sqrt(2.0) * 7.0) / 4.0) expectFailureWhenParsingNumericExpression("1+2 &asdf") @@ -683,6 +712,10 @@ class NumericExpressionParserTest { } } } + assertThat(expression21) + .evaluatesToIrrationalThat() + .isWithin(1e-5) + .of(sqrt(2.0) * sqrt(3.0) * sqrt(4.0)) val expression22 = parseNumericExpressionWithAllErrors("(1+2)(3-7^2)(5+-17)") assertThat(expression22).hasStructureThatMatches { @@ -757,6 +790,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression22).evaluatesToIntegerThat().isEqualTo(1656) val expression26 = parseNumericExpressionWithAllErrors("3^-2") assertThat(expression26).hasStructureThatMatches { @@ -777,6 +811,12 @@ class NumericExpressionParserTest { } } } + assertThat(expression26).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(9) + } val expression27 = parseNumericExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") assertThat(expression27).hasStructureThatMatches { @@ -823,6 +863,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression27).evaluatesToIrrationalThat().isWithin(1e-5).of(0.78338103693) val expression28 = parseNumericExpressionWithAllErrors("1-3^sqrt(4)") assertThat(expression28).hasStructureThatMatches { @@ -852,6 +893,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression28).evaluatesToIntegerThat().isEqualTo(-8) // "Hard" order of operation problems loosely based on & other problems that can often stump // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. @@ -890,6 +932,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression29).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) val expression59 = parseNumericExpressionWithAllErrors("3÷2(3+4)") assertThat(expression59).hasStructureThatMatches { @@ -926,6 +969,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression59).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) // Numbers cannot have implicit multiplication unless they are in groups. expectFailureWhenParsingNumericExpression("2 2") @@ -964,6 +1008,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression31).evaluatesToIntegerThat().isEqualTo(60) val expression33 = parseNumericExpressionWithoutOptionalErrors("2^(3)") assertThat(expression33).hasStructureThatMatches { @@ -982,6 +1027,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression33).evaluatesToIntegerThat().isEqualTo(8) // Verify that implicit multiple has lower precedence than exponentiation. val expression34 = parseNumericExpressionWithoutOptionalErrors("2^(3)(4)") @@ -1012,6 +1058,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression34).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can never be an implicit right operand. expectFailureWhenParsingNumericExpression("2^(3)2^2") @@ -1051,6 +1098,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression35).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can be a right operand of an implicit mult if it's grouped. val expression36 = parseNumericExpressionWithoutOptionalErrors("2^(3)(2^2)") @@ -1090,6 +1138,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression36).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can never be an implicit right operand. expectFailureWhenParsingNumericExpression("2^3(4)2^3") @@ -1147,6 +1196,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression38).evaluatesToIntegerThat().isEqualTo(256) expectFailureWhenParsingNumericExpression("2^2 2^2") expectFailureWhenParsingNumericExpression("(3) 2^2") @@ -1177,6 +1227,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression39).evaluatesToIntegerThat().isEqualTo(-3) // Should pass for algebra. expectFailureWhenParsingNumericExpression("-2 x") @@ -1212,6 +1263,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression40).evaluatesToIntegerThat().isEqualTo(-6) val expression41 = parseNumericExpressionWithoutOptionalErrors("-2^3(4)") assertThat(expression41).hasStructureThatMatches { @@ -1243,6 +1295,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression41).evaluatesToIntegerThat().isEqualTo(-32) val expression43 = parseNumericExpressionWithoutOptionalErrors("√2^2(3)") assertThat(expression43).hasStructureThatMatches { @@ -1274,6 +1327,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression43).evaluatesToIrrationalThat().isWithin(1e-5).of(6.0) val expression60 = parseNumericExpressionWithoutOptionalErrors("√(2^2(3))") assertThat(expression60).hasStructureThatMatches { @@ -1307,6 +1361,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression60).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(12.0)) val expression42 = parseNumericExpressionWithAllErrors("-2*-2") // Note that the following structure is not the same as (-2)*(-2) since unary negation has @@ -1334,6 +1389,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression42).evaluatesToIntegerThat().isEqualTo(4) // TODO: Here & elsewhere, fix the fact that this is actually a valid use of single-term // parentheses (there's a bug in the current error detection logic). @@ -1354,6 +1410,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression44).evaluatesToIntegerThat().isEqualTo(4) val expression45 = parseNumericExpressionWithAllErrors("2sqrt(2)") assertThat(expression45).hasStructureThatMatches { @@ -1374,6 +1431,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression45).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) val expression46 = parseNumericExpressionWithAllErrors("2√2") assertThat(expression46).hasStructureThatMatches { @@ -1394,6 +1452,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression46).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) val expression47 = parseNumericExpressionWithoutOptionalErrors("(2)(2)") assertThat(expression47).hasStructureThatMatches { @@ -1414,6 +1473,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression47).evaluatesToIntegerThat().isEqualTo(4) val expression48 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)(2)") assertThat(expression48).hasStructureThatMatches { @@ -1436,6 +1496,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression48).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) val expression49 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(2)") assertThat(expression49).hasStructureThatMatches { @@ -1460,6 +1521,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression49).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) val expression50 = parseNumericExpressionWithAllErrors("√2√2") assertThat(expression50).hasStructureThatMatches { @@ -1484,6 +1546,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression50).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) val expression51 = parseNumericExpressionWithoutOptionalErrors("(2)(2)(2)") assertThat(expression51).hasStructureThatMatches { @@ -1515,6 +1578,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression51).evaluatesToIntegerThat().isEqualTo(8) val expression52 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(2)sqrt(2)") assertThat(expression52).hasStructureThatMatches { @@ -1552,6 +1616,8 @@ class NumericExpressionParserTest { } } } + val sqrt2 = sqrt(2.0) + assertThat(expression52).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) val expression53 = parseNumericExpressionWithAllErrors("√2√2√2") assertThat(expression53).hasStructureThatMatches { @@ -1589,6 +1655,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression53).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) // Should fail for algebra. expectFailureWhenParsingNumericExpression("x7") @@ -1652,6 +1719,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression54).evaluatesToIntegerThat().isEqualTo(13) val expression55 = parseNumericExpressionWithAllErrors("3/(1-2)") assertThat(expression55).hasStructureThatMatches { @@ -1679,6 +1747,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression55).evaluatesToIntegerThat().isEqualTo(-3) val expression56 = parseNumericExpressionWithoutOptionalErrors("(3)/(1-2)") assertThat(expression56).hasStructureThatMatches { @@ -1708,6 +1777,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression56).evaluatesToIntegerThat().isEqualTo(-3) val expression57 = parseNumericExpressionWithoutOptionalErrors("3/((1-2))") assertThat(expression57).hasStructureThatMatches { @@ -1737,6 +1807,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression57).evaluatesToIntegerThat().isEqualTo(-3) // TODO: add others, including tests for malformed expressions throughout the parser & // tokenizer. From d9d4963fc292d23bb765a95170b1fe9bae10f855 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 11:29:25 -0800 Subject: [PATCH 110/289] Remove unneeded comment lines. --- .../src/test/java/org/oppia/android/util/math/BUILD.bazel | 7 ------- 1 file changed, 7 deletions(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 2a8b0c295c4..c4791dffc69 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -4,13 +4,6 @@ TODO: document load("//:oppia_android_test.bzl", "oppia_android_test") -# "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_list_subject", -# "//testing/src/main/java/org/oppia/android/testing/math:fraction_subject", -# "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", -# "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", -# "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", -# "//testing/src/main/java/org/oppia/android/testing/math:real_subject", - oppia_android_test( name = "AlgebraicEquationParserTest", srcs = ["AlgebraicEquationParserTest.kt"], From 1a8a8e85ce1c15f0e3565d10aa1392136ee94e7f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 13:39:57 -0800 Subject: [PATCH 111/289] Add expr->comparable operation list conv support. This enables the ability to compare two expressions such that operation associativity and commutativity is considered (i.e. items can be rearranged using those rules without breaking expression equality). This is mostly copied from #2173. --- .../org/oppia/android/util/math/BUILD.bazel | 21 + .../android/util/math/ComparatorExtensions.kt | 57 + ...ssionToComparableOperationListConverter.kt | 300 +++ .../util/math/MathExpressionExtensions.kt | 35 + .../oppia/android/util/math/RealExtensions.kt | 2 + .../org/oppia/android/util/math/BUILD.bazel | 19 + ...ExpressionToComparableOperationListTest.kt | 1637 +++++++++++++++++ 7 files changed, 2071 insertions(+) create mode 100644 utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListTest.kt diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 351b04e5f33..fbd04084430 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -10,6 +10,7 @@ android_library( "//:oppia_api_visibility", ], exports = [ + ":comparator_extensions", ":float_extensions", ":fraction_extensions", ":math_expression_extensions", @@ -70,6 +71,13 @@ kt_android_library( ], ) +kt_android_library( + name = "comparator_extensions", + srcs = [ + "ComparatorExtensions.kt", + ], +) + kt_android_library( name = "float_extensions", srcs = [ @@ -96,6 +104,7 @@ kt_android_library( "MathExpressionExtensions.kt", ], deps = [ + ":expression_to_comparable_operation_list_converter", ":expression_to_latex_converter", ":numeric_expression_evaluator", "//model/src/main/proto:math_java_proto_lite", @@ -136,6 +145,18 @@ kt_android_library( ], ) +kt_android_library( + name = "expression_to_comparable_operation_list_converter", + srcs = [ + "ExpressionToComparableOperationListConverter.kt", + ], + deps = [ + ":comparator_extensions", + ":real_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + kt_android_library( name = "expression_to_latex_converter", srcs = [ diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt new file mode 100644 index 00000000000..284ec69fe69 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt @@ -0,0 +1,57 @@ +package org.oppia.android.util.math + +import java.util.SortedSet + +fun comparingDeferred( + keySelector: (T) -> U, + comparatorSelector: () -> Comparator +): Comparator { + // Store as captured val for memoization. + val comparator by lazy { comparatorSelector() } + return Comparator.comparing(keySelector) { o1, o2 -> + comparator.compare(o1, o2) + } +} + +fun > Comparator.thenComparingReversed( + keySelector: (T) -> U +): Comparator = thenComparing(Comparator.comparing(keySelector).reversed()) + +fun > Comparator.thenSelectAmong( + enumSelector: (T) -> E, + vararg comparators: Pair> +): Comparator { + val comparatorMap = comparators.toMap() + return thenComparing( + Comparator { o1, o2 -> + val enum1 = enumSelector(o1) + val enum2 = enumSelector(o2) + check(enum1 == enum2) { + "Expected objects to have the same enum values: $o1 ($enum1), $o2 ($enum2)" + } + val comparator = + checkNotNull(comparatorMap[enum1]) { "No comparator for matched enum: $enum1" } + return@Comparator comparator.compare(o1, o2) + } + ) +} + +fun Comparator.toSetComparator(): Comparator> { + val itemComparator = this + return Comparator { first, second -> + // Reference: https://stackoverflow.com/a/30107086. + val firstIter = first.iterator() + val secondIter = second.iterator() + while (firstIter.hasNext() && secondIter.hasNext()) { + val comparison = itemComparator.compare(firstIter.next(), secondIter.next()) + if (comparison != 0) return@Comparator comparison // Found a different item. + } + + // Everything is equal up to here, see if the lists are different length. + return@Comparator when { + firstIter.hasNext() -> 1 // The first list is longer, therefore "greater." + secondIter.hasNext() -> -1 // Ditto, but for the second list. + else -> 0 // Otherwise, they're the same length with all equal items (and are thus equal). + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt new file mode 100644 index 00000000000..4cfa9acec66 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt @@ -0,0 +1,300 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.ACCUMULATION_TYPE_UNSPECIFIED +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.PRODUCT +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.COMPARISONTYPE_NOT_SET +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.CONSTANT_TERM +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.VARIABLE_TERM +import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation +import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation.OperationTypeCase +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall.FunctionType +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator + +class ExpressionToComparableOperationListConverter private constructor() { + companion object { + private val COMPARABLE_OPERATION_COMPARATOR: Comparator by lazy { + // Some of the comparators must be deferred since they indirectly reference this comparator + // (which isn't valid until it's fully assembled). + Comparator.comparing(ComparableOperation::getComparisonTypeCase) + .thenComparing(ComparableOperation::getIsNegated) + .thenComparing(ComparableOperation::getIsInverted) + .thenSelectAmong( + ComparableOperation::getComparisonTypeCase, + ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION to comparingDeferred( + ComparableOperation::getCommutativeAccumulation + ) { COMMUTATIVE_ACCUMULATION_COMPARATOR }, + NON_COMMUTATIVE_OPERATION to comparingDeferred( + ComparableOperation::getNonCommutativeOperation + ) { NON_COMMUTATIVE_OPERATION_COMPARATOR }, + CONSTANT_TERM to Comparator.comparing( + ComparableOperation::getConstantTerm, REAL_COMPARATOR + ), + VARIABLE_TERM to Comparator.comparing(ComparableOperation::getVariableTerm) + ) + } + + private val COMMUTATIVE_ACCUMULATION_COMPARATOR: Comparator by lazy { + Comparator.comparing(CommutativeAccumulation::getAccumulationType) + .thenComparing( + { accumulation -> + accumulation.combinedOperationsList.toSortedSet(COMPARABLE_OPERATION_COMPARATOR) + }, + COMPARABLE_OPERATION_COMPARATOR.toSetComparator() + ) + } + + private val NON_COMMUTATIVE_BINARY_OPERATION_COMPARATOR by lazy { + Comparator.comparing( + NonCommutativeOperation.BinaryOperation::getLeftOperand, COMPARABLE_OPERATION_COMPARATOR + ).thenComparing( + NonCommutativeOperation.BinaryOperation::getRightOperand, COMPARABLE_OPERATION_COMPARATOR + ) + } + + private val NON_COMMUTATIVE_OPERATION_COMPARATOR: Comparator by lazy { + Comparator.comparing(NonCommutativeOperation::getOperationTypeCase) + .thenSelectAmong( + NonCommutativeOperation::getOperationTypeCase, + OperationTypeCase.EXPONENTIATION to Comparator.comparing( + NonCommutativeOperation::getExponentiation, NON_COMMUTATIVE_BINARY_OPERATION_COMPARATOR + ), + OperationTypeCase.SQUARE_ROOT to Comparator.comparing( + NonCommutativeOperation::getSquareRoot, COMPARABLE_OPERATION_COMPARATOR + ), + ) + } + + fun MathExpression.toComparable(): ComparableOperationList { + return ComparableOperationList.newBuilder().apply { + rootOperation = toComparableOperation().stabilizeNegation().sort() + }.build() + } + + private fun MathExpression.toComparableOperation(): ComparableOperation { + return when (expressionTypeCase) { + CONSTANT -> ComparableOperation.newBuilder().apply { + constantTerm = constant + }.build() + VARIABLE -> ComparableOperation.newBuilder().apply { + variableTerm = variable + }.build() + BINARY_OPERATION -> when (binaryOperation.operator) { + ADD -> toSummation(isRhsNegative = false) + SUBTRACT -> toSummation(isRhsNegative = true) + MULTIPLY -> toProduct(isRhsInverted = false) + DIVIDE -> toProduct(isRhsInverted = true) + EXPONENTIATE -> + toNonCommutativeOperation(NonCommutativeOperation.Builder::setExponentiation) + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> + ComparableOperation.getDefaultInstance() + } + UNARY_OPERATION -> when (unaryOperation.operator) { + NEGATE -> unaryOperation.operand.toComparableOperation().makeNegative() + POSITIVE -> unaryOperation.operand.toComparableOperation() + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> + ComparableOperation.getDefaultInstance() + } + FUNCTION_CALL -> when (functionCall.functionType) { + SQUARE_ROOT -> ComparableOperation.newBuilder().apply { + nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { + squareRoot = functionCall.argument.toComparableOperation() + }.build() + }.build() + FunctionType.FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> + ComparableOperation.getDefaultInstance() + } + GROUP -> group.toComparableOperation() + EXPRESSIONTYPE_NOT_SET, null -> ComparableOperation.getDefaultInstance() + } + } + + private fun MathExpression.toSummation(isRhsNegative: Boolean): ComparableOperation { + return ComparableOperation.newBuilder().apply { + commutativeAccumulation = CommutativeAccumulation.newBuilder().apply { + accumulationType = SUMMATION + addOperationToSum(binaryOperation.leftOperand, forceNegative = false) + addOperationToSum(binaryOperation.rightOperand, forceNegative = isRhsNegative) + }.build() + }.build() + } + + private fun MathExpression.toProduct(isRhsInverted: Boolean): ComparableOperation { + return ComparableOperation.newBuilder().apply { + commutativeAccumulation = CommutativeAccumulation.newBuilder().apply { + accumulationType = PRODUCT + addOperationToProduct(binaryOperation.leftOperand, forceInverse = false) + addOperationToProduct(binaryOperation.rightOperand, forceInverse = isRhsInverted) + }.build() + }.build() + } + + private fun CommutativeAccumulation.Builder.addOperationToSum( + expression: MathExpression, + forceNegative: Boolean + ) { + when (expression.binaryOperation.operator) { + ADD -> { + // If the whole operation is negative, carry it to the left-hand side of the operation. + addOperationToSum(expression.binaryOperation.leftOperand, forceNegative) + addOperationToSum(expression.binaryOperation.rightOperand, forceNegative = false) + } + SUBTRACT -> { + addOperationToSum(expression.binaryOperation.leftOperand, forceNegative) + addOperationToSum(expression.binaryOperation.rightOperand, forceNegative = true) + } + else -> if (forceNegative) { + addCombinedOperations(expression.toComparableOperation().makeNegative()) + } else addCombinedOperations(expression.toComparableOperation()) + } + } + + private fun CommutativeAccumulation.Builder.addOperationToProduct( + expression: MathExpression, + forceInverse: Boolean + ) { + when (expression.binaryOperation.operator) { + MULTIPLY -> { + // If the whole operation is inverted, carry it to the left-hand side of the operation. + addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) + addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = false) + } + DIVIDE -> { + addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) + addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = true) + } + else -> if (forceInverse) { + addCombinedOperations(expression.toComparableOperation().makeInverted()) + } else addCombinedOperations(expression.toComparableOperation()) + } + } + + private fun MathExpression.toNonCommutativeOperation( + setOperation: NonCommutativeOperation.Builder.( + NonCommutativeOperation.BinaryOperation + ) -> NonCommutativeOperation.Builder + ): ComparableOperation { + return ComparableOperation.newBuilder().apply { + nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { + setOperation( + NonCommutativeOperation.BinaryOperation.newBuilder().apply { + leftOperand = binaryOperation.leftOperand.toComparableOperation() + rightOperand = binaryOperation.rightOperand.toComparableOperation() + }.build() + ) + }.build() + }.build() + } + + private fun ComparableOperation.makePositive(): ComparableOperation = + toBuilder().apply { isNegated = false }.build() + + private fun ComparableOperation.makeNegative(): ComparableOperation = + toBuilder().apply { isNegated = true }.build() + + private fun ComparableOperation.makeInverted(): ComparableOperation = + toBuilder().apply { isInverted = true }.build() + + private fun ComparableOperation.stabilizeNegation(): ComparableOperation { + return when (comparisonTypeCase) { + ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION -> { + val stabilizedOperations = + commutativeAccumulation.combinedOperationsList.map { it.stabilizeNegation() } + when (commutativeAccumulation.accumulationType) { + SUMMATION -> toBuilder().apply { + commutativeAccumulation = commutativeAccumulation.toBuilder().apply { + clearCombinedOperations() + addAllCombinedOperations(stabilizedOperations) + }.build() + }.build() + PRODUCT -> { + // Negations can be combined for all constituent operations & brought up to the + // top-level operation. + val negativeCount = stabilizedOperations.count { + it.isNegated + } + if (isNegated) 1 else 0 + val positiveOperations = stabilizedOperations.map { it.makePositive() } + toBuilder().apply { + isNegated = (negativeCount % 2) == 1 + commutativeAccumulation = commutativeAccumulation.toBuilder().apply { + clearCombinedOperations() + addAllCombinedOperations(positiveOperations) + }.build() + }.build() + } + ACCUMULATION_TYPE_UNSPECIFIED, AccumulationType.UNRECOGNIZED, null -> this + } + } + NON_COMMUTATIVE_OPERATION -> toBuilder().apply { + // Negation can't be extracted from commutative operations. + nonCommutativeOperation = when (nonCommutativeOperation.operationTypeCase) { + OperationTypeCase.EXPONENTIATION -> nonCommutativeOperation.toBuilder().apply { + exponentiation = nonCommutativeOperation.exponentiation.toBuilder().apply { + leftOperand = nonCommutativeOperation.exponentiation.leftOperand.stabilizeNegation() + rightOperand = + nonCommutativeOperation.exponentiation.rightOperand.stabilizeNegation() + }.build() + }.build() + OperationTypeCase.SQUARE_ROOT -> nonCommutativeOperation.toBuilder().apply { + squareRoot = nonCommutativeOperation.squareRoot.stabilizeNegation() + }.build() + OperationTypeCase.OPERATIONTYPE_NOT_SET, null -> nonCommutativeOperation + } + }.build() + CONSTANT_TERM -> this + VARIABLE_TERM -> this + COMPARISONTYPE_NOT_SET, null -> this + } + } + + private fun ComparableOperation.sort(): ComparableOperation { + return when (comparisonTypeCase) { + ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION -> toBuilder().apply { + commutativeAccumulation = commutativeAccumulation.toBuilder().apply { + clearCombinedOperations() + // Sort the operations themselves before sorting them relative to each other. + val innerSortedList = commutativeAccumulation.combinedOperationsList.map { it.sort() } + addAllCombinedOperations(innerSortedList.sortedWith(COMPARABLE_OPERATION_COMPARATOR)) + }.build() + }.build() + NON_COMMUTATIVE_OPERATION -> toBuilder().apply { + nonCommutativeOperation = when (nonCommutativeOperation.operationTypeCase) { + OperationTypeCase.EXPONENTIATION -> nonCommutativeOperation.toBuilder().apply { + exponentiation = nonCommutativeOperation.exponentiation.toBuilder().apply { + leftOperand = nonCommutativeOperation.exponentiation.leftOperand.sort() + rightOperand = nonCommutativeOperation.exponentiation.rightOperand.sort() + }.build() + }.build() + OperationTypeCase.SQUARE_ROOT -> nonCommutativeOperation.toBuilder().apply { + squareRoot = nonCommutativeOperation.squareRoot.sort() + }.build() + OperationTypeCase.OPERATIONTYPE_NOT_SET, null -> nonCommutativeOperation + } + }.build() + CONSTANT_TERM, VARIABLE_TERM, COMPARISONTYPE_NOT_SET, null -> this + } + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 59b203a0d2d..70fa465e30a 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -1,8 +1,17 @@ package org.oppia.android.util.math +import org.oppia.android.app.model.ComparableOperationList import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.Real +import org.oppia.android.util.math.ExpressionToComparableOperationListConverter.Companion.toComparable import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate @@ -11,3 +20,29 @@ fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(div fun MathExpression.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() + +fun MathExpression.toComparableOperationList(): ComparableOperationList = + stripGroups().toComparable() + +private fun MathExpression.stripGroups(): MathExpression { + return when (expressionTypeCase) { + BINARY_OPERATION -> toBuilder().apply { + binaryOperation = binaryOperation.toBuilder().apply { + leftOperand = binaryOperation.leftOperand.stripGroups() + rightOperand = binaryOperation.rightOperand.stripGroups() + }.build() + }.build() + UNARY_OPERATION -> toBuilder().apply { + unaryOperation = unaryOperation.toBuilder().apply { + operand = unaryOperation.operand.stripGroups() + }.build() + }.build() + FUNCTION_CALL -> toBuilder().apply { + functionCall = functionCall.toBuilder().apply { + argument = functionCall.argument.stripGroups() + }.build() + }.build() + GROUP -> group.stripGroups() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> this + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 83605b05808..cbe17f0dc92 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -8,6 +8,8 @@ import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import kotlin.math.pow +val REAL_COMPARATOR: Comparator by lazy { Comparator.comparing(Real::toDouble) } + fun Real.isRational(): Boolean = realTypeCase == RATIONAL fun Real.isInteger(): Boolean = realTypeCase == INTEGER diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 69d9dab509a..fcce3805364 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -42,6 +42,25 @@ oppia_android_test( ], ) +oppia_android_test( + name = "ExpressionToComparableOperationListTest", + srcs = ["ExpressionToComparableOperationListTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.ExpressionToComparableOperationListTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_list_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + "//utility/src/main/java/org/oppia/android/util/math:parser", + ], +) + oppia_android_test( name = "ExpressionToLatexTest", srcs = ["ExpressionToLatexTest.kt"], diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListTest.kt new file mode 100644 index 00000000000..d9599c0b32d --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListTest.kt @@ -0,0 +1,1637 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.PRODUCT +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION +import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.math.ComparableOperationListSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +// SameParameterValue: tests should have specific context included/excluded for readability. +@Suppress("SameParameterValue") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ExpressionToComparableOperationListTest { + // TODO: add high-level checks for the three types, but don't test in detail since there are + // separate suites. Also, document the separate suites' existence in this suites's KDoc. + + @Test + fun testToComparableOperation() { + // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). + + val exp1 = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp1.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + + val exp2 = parseNumericExpressionSuccessfullyWithAllErrors("-1") + assertThat(exp2.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + + val exp3 = parseNumericExpressionSuccessfullyWithAllErrors("1+3+4") + assertThat(exp3.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp4 = parseNumericExpressionSuccessfullyWithAllErrors("-1-2-3") + assertThat(exp4.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp5 = parseNumericExpressionSuccessfullyWithAllErrors("1+2-3") + assertThat(exp5.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp6 = parseNumericExpressionSuccessfullyWithAllErrors("2*3*4") + assertThat(exp6.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp7 = parseNumericExpressionSuccessfullyWithAllErrors("1-2*3") + assertThat(exp7.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + + val exp8 = parseNumericExpressionSuccessfullyWithAllErrors("2*3-4") + assertThat(exp8.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp9 = parseNumericExpressionSuccessfullyWithAllErrors("1+2*3-4+8*7*6-9") + assertThat(exp9.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(3) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(9) + } + } + } + } + + val exp10 = parseNumericExpressionSuccessfullyWithAllErrors("2/3/4") + assertThat(exp10.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp11 = parseNumericExpressionSuccessfullyWithoutOptionalErrors("2^3^4") + assertThat(exp11.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + val exp12 = parseNumericExpressionSuccessfullyWithAllErrors("1+2/3+3") + assertThat(exp12.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp13 = parseNumericExpressionSuccessfullyWithAllErrors("1+(2/3)+3") + assertThat(exp13.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp14 = parseNumericExpressionSuccessfullyWithAllErrors("1+2^3+3") + assertThat(exp14.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp15 = parseNumericExpressionSuccessfullyWithAllErrors("1+(2^3)+3") + assertThat(exp15.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp16 = parseNumericExpressionSuccessfullyWithAllErrors("2*3/4*7") + assertThat(exp16.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp17 = parseNumericExpressionSuccessfullyWithAllErrors("2*(3/4)*7") + assertThat(exp17.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp18 = parseNumericExpressionSuccessfullyWithAllErrors("-3*sqrt(2)") + assertThat(exp18.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + squareRootWithArgument { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp19 = parseNumericExpressionSuccessfullyWithAllErrors("1+(2+(3+(4+5)))") + assertThat(exp19.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + + val exp20 = parseNumericExpressionSuccessfullyWithAllErrors("2*(3*(4*(5*6)))") + assertThat(exp20.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + } + } + + val exp21 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp21.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + + val exp22 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-x") + assertThat(exp22.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + + val exp23 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x+y") + assertThat(exp23.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp24 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-1-x-y") + assertThat(exp24.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp25 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x-y") + assertThat(exp25.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp26 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xy") + assertThat(exp26.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp27 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1-xy") + assertThat(exp27.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + + val exp28 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy-4") + assertThat(exp28.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp29 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+xy-4+yz-9") + assertThat(exp29.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(3) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(9) + } + } + } + } + + val exp30 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2/x/y") + assertThat(exp30.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp31 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("x^3^4") + assertThat(exp31.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + val exp32 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x/y+z") + assertThat(exp32.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + val exp33 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x/y)+z") + assertThat(exp33.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + val exp34 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x^3+y") + assertThat(exp34.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp35 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x^3)+y") + assertThat(exp35.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp36 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*x/y*z") + assertThat(exp36.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp37 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(x/y)*z") + assertThat(exp37.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp38 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-2*sqrt(x)") + assertThat(exp38.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + squareRootWithArgument { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + val exp39 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x+(3+(z+y)))") + assertThat(exp39.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + val exp40 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(x*(4*(zy)))") + assertThat(exp40.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + // Equality tests: + val list1 = createComparableOperationListFromNumericExpression("(1+2)+3") + val list2 = createComparableOperationListFromNumericExpression("1+(2+3)") + assertThat(list1).isEqualTo(list2) + + val list3 = createComparableOperationListFromNumericExpression("1+2+3") + val list4 = createComparableOperationListFromNumericExpression("3+2+1") + assertThat(list3).isEqualTo(list4) + + val list5 = createComparableOperationListFromNumericExpression("1-2-3") + val list6 = createComparableOperationListFromNumericExpression("-3 + -2 + 1") + assertThat(list5).isEqualTo(list6) + + val list7 = createComparableOperationListFromNumericExpression("1-2-3") + val list8 = createComparableOperationListFromNumericExpression("-3-2+1") + assertThat(list7).isEqualTo(list8) + + val list9 = createComparableOperationListFromNumericExpression("1-2-3") + val list10 = createComparableOperationListFromNumericExpression("-3-2+1") + assertThat(list9).isEqualTo(list10) + + val list11 = createComparableOperationListFromNumericExpression("1-2-3") + val list12 = createComparableOperationListFromNumericExpression("3-2-1") + assertThat(list11).isNotEqualTo(list12) + + val list13 = createComparableOperationListFromNumericExpression("2*3*4") + val list14 = createComparableOperationListFromNumericExpression("4*3*2") + assertThat(list13).isEqualTo(list14) + + val list15 = createComparableOperationListFromNumericExpression("2*(3/4)") + val list16 = createComparableOperationListFromNumericExpression("3/4*2") + assertThat(list15).isEqualTo(list16) + + val list17 = createComparableOperationListFromNumericExpression("2*3/4") + val list18 = createComparableOperationListFromNumericExpression("3/4*2") + assertThat(list17).isEqualTo(list18) + + val list45 = createComparableOperationListFromNumericExpression("2*3/4") + val list46 = createComparableOperationListFromNumericExpression("2*3*4") + assertThat(list45).isNotEqualTo(list46) + + val list19 = createComparableOperationListFromNumericExpression("2*3/4") + val list20 = createComparableOperationListFromNumericExpression("2*4/3") + assertThat(list19).isNotEqualTo(list20) + + val list21 = createComparableOperationListFromNumericExpression("2*3/4*7") + val list22 = createComparableOperationListFromNumericExpression("3/4*7*2") + assertThat(list21).isEqualTo(list22) + + val list23 = createComparableOperationListFromNumericExpression("2*3/4*7") + val list24 = createComparableOperationListFromNumericExpression("7*(3*2/4)") + assertThat(list23).isEqualTo(list24) + + val list25 = createComparableOperationListFromNumericExpression("2*3/4*7") + val list26 = createComparableOperationListFromNumericExpression("7*3*2/4") + assertThat(list25).isEqualTo(list26) + + val list27 = createComparableOperationListFromNumericExpression("-2*3") + val list28 = createComparableOperationListFromNumericExpression("3*-2") + assertThat(list27).isEqualTo(list28) + + val list29 = createComparableOperationListFromNumericExpression("2^3") + val list30 = createComparableOperationListFromNumericExpression("3^2") + assertThat(list29).isNotEqualTo(list30) + + val list31 = createComparableOperationListFromNumericExpression("-(1+2)") + val list32 = createComparableOperationListFromNumericExpression("-1+2") + assertThat(list31).isNotEqualTo(list32) + + val list33 = createComparableOperationListFromNumericExpression("-(1+2)") + val list34 = createComparableOperationListFromNumericExpression("-1-2") + assertThat(list33).isNotEqualTo(list34) + + val list35 = createComparableOperationListFromAlgebraicExpression("x(x+1)") + val list36 = createComparableOperationListFromAlgebraicExpression("(1+x)x") + assertThat(list35).isEqualTo(list36) + + val list37 = createComparableOperationListFromAlgebraicExpression("x(x+1)") + val list38 = createComparableOperationListFromAlgebraicExpression("x^2+x") + assertThat(list37).isNotEqualTo(list38) + + val list39 = createComparableOperationListFromAlgebraicExpression("x^2*sqrt(x)") + val list40 = createComparableOperationListFromAlgebraicExpression("x") + assertThat(list39).isNotEqualTo(list40) + + val list41 = createComparableOperationListFromAlgebraicExpression("xyz") + val list42 = createComparableOperationListFromAlgebraicExpression("zyx") + assertThat(list41).isEqualTo(list42) + + val list43 = createComparableOperationListFromAlgebraicExpression("1+xy-2") + val list44 = createComparableOperationListFromAlgebraicExpression("-2+1+yx") + assertThat(list43).isEqualTo(list44) + + // TODO: add tests for comparator/sorting & negation simplification? + } + + private fun createComparableOperationListFromNumericExpression(expression: String) = + parseNumericExpressionSuccessfullyWithAllErrors(expression).toComparableOperationList() + + private fun createComparableOperationListFromAlgebraicExpression(expression: String) = + parseAlgebraicExpressionSuccessfullyWithAllErrors(expression).toComparableOperationList() + + private companion object { + // TODO: fix helper API. + + private fun parseNumericExpressionSuccessfullyWithAllErrors( + expression: String + ): MathExpression { + val result = parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionSuccessfullyWithoutOptionalErrors( + expression: String + ): MathExpression { + val result = + parseNumericExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY + ) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode + ): MathParsingResult { + return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) + } + + private fun parseAlgebraicExpressionSuccessfullyWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) + } + } +} From 917c9093c052df08eaa838e109c588c0d277f319 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 14:41:11 -0800 Subject: [PATCH 112/289] Add support for expression->polynomial conversion. This is mostly copied from #2173. --- .../org/oppia/android/util/math/BUILD.bazel | 14 + .../math/ExpressionToPolynomialConverter.kt | 110 ++ .../util/math/MathExpressionExtensions.kt | 4 + .../android/util/math/PolynomialExtensions.kt | 307 ++++++ .../oppia/android/util/math/RealExtensions.kt | 44 +- .../org/oppia/android/util/math/BUILD.bazel | 18 + .../util/math/ExpressionToPolynomialTest.kt | 945 ++++++++++++++++++ 7 files changed, 1438 insertions(+), 4 deletions(-) create mode 100644 utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index fbd04084430..3bdacb733c2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -106,6 +106,7 @@ kt_android_library( deps = [ ":expression_to_comparable_operation_list_converter", ":expression_to_latex_converter", + ":expression_to_polynomial_converter", ":numeric_expression_evaluator", "//model/src/main/proto:math_java_proto_lite", ], @@ -117,6 +118,7 @@ kt_android_library( "PolynomialExtensions.kt", ], deps = [ + ":comparator_extensions", ":real_extensions", "//model/src/main/proto:math_java_proto_lite", ], @@ -168,6 +170,18 @@ kt_android_library( ], ) +kt_android_library( + name = "expression_to_polynomial_converter", + srcs = [ + "ExpressionToPolynomialConverter.kt", + ], + deps = [ + ":polynomial_extensions", + ":real_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + kt_android_library( name = "numeric_expression_evaluator", srcs = [ diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt new file mode 100644 index 00000000000..d16ac87fce1 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt @@ -0,0 +1,110 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall.FunctionType +import org.oppia.android.app.model.MathFunctionCall.FunctionType.FUNCTION_UNSPECIFIED +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.Polynomial +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator + +class ExpressionToPolynomialConverter private constructor() { + companion object { + fun MathExpression.reduceToPolynomial(): Polynomial? = + replaceSquareRoots().reduceToPolynomialAux()?.removeUnnecessaryVariables()?.sort() + + private fun MathExpression.replaceSquareRoots(): MathExpression { + return when (expressionTypeCase) { + BINARY_OPERATION -> toBuilder().apply { + binaryOperation = binaryOperation.toBuilder().apply { + leftOperand = binaryOperation.leftOperand.replaceSquareRoots() + rightOperand = binaryOperation.rightOperand.replaceSquareRoots() + }.build() + }.build() + UNARY_OPERATION -> toBuilder().apply { + unaryOperation = unaryOperation.toBuilder().apply { + operand = unaryOperation.operand.replaceSquareRoots() + }.build() + }.build() + FUNCTION_CALL -> when (functionCall.functionType) { + SQUARE_ROOT -> toBuilder().apply { + // Replace the square root function call with the equivalent exponentiation. That is, + // sqrt(x)=x^(1/2). + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = EXPONENTIATE + leftOperand = functionCall.argument.replaceSquareRoots() + rightOperand = MathExpression.newBuilder().apply { + constant = ONE_HALF + }.build() + }.build() + }.build() + FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> this + } + GROUP -> group.replaceSquareRoots() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> this + } + } + + private fun MathExpression.reduceToPolynomialAux(): Polynomial? { + return when (expressionTypeCase) { + CONSTANT -> createConstantPolynomial(constant) + VARIABLE -> createSingleVariablePolynomial(variable) + BINARY_OPERATION -> binaryOperation.reduceToPolynomial() + UNARY_OPERATION -> unaryOperation.reduceToPolynomial() + // Both functions & groups should be removed ahead of polynomial reduction. + FUNCTION_CALL, GROUP, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathBinaryOperation.reduceToPolynomial(): Polynomial? { + val leftPolynomial = leftOperand.reduceToPolynomialAux() ?: return null + val rightPolynomial = rightOperand.reduceToPolynomialAux() ?: return null + return when (operator) { + ADD -> leftPolynomial + rightPolynomial + SUBTRACT -> leftPolynomial - rightPolynomial + MULTIPLY -> leftPolynomial * rightPolynomial + DIVIDE -> leftPolynomial / rightPolynomial + EXPONENTIATE -> leftPolynomial.pow(rightPolynomial) + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null + } + } + + private fun MathUnaryOperation.reduceToPolynomial(): Polynomial? { + return when (operator) { + NEGATE -> -(operand.reduceToPolynomialAux() ?: return null) + POSITIVE -> operand.reduceToPolynomialAux() // Positive unary changes nothing. + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> null + } + } + + private fun createSingleVariablePolynomial(variableName: String): Polynomial { + return createSingleTermPolynomial( + Polynomial.Term.newBuilder().apply { + coefficient = ONE + addVariable( + Polynomial.Term.Variable.newBuilder().apply { + name = variableName + power = 1 + }.build() + ) + }.build() + ) + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 70fa465e30a..39e59ce99bb 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -10,9 +10,11 @@ import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CA import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Real import org.oppia.android.util.math.ExpressionToComparableOperationListConverter.Companion.toComparable import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex +import org.oppia.android.util.math.ExpressionToPolynomialConverter.Companion.reduceToPolynomial import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) @@ -24,6 +26,8 @@ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() fun MathExpression.toComparableOperationList(): ComparableOperationList = stripGroups().toComparable() +fun MathExpression.toPolynomial(): Polynomial? = stripGroups().reduceToPolynomial() + private fun MathExpression.stripGroups(): MathExpression { return when (expressionTypeCase) { BINARY_OPERATION -> toBuilder().apply { diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index a4ba72213be..74b1187b6e0 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -1,13 +1,43 @@ package org.oppia.android.util.math +import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Polynomial.Term import org.oppia.android.app.model.Polynomial.Term.Variable import org.oppia.android.app.model.Real +import java.util.SortedSet + +private val POLYNOMIAL_VARIABLE_COMPARATOR: Comparator by lazy { + // Note that power is reversed because larger powers should actually be sorted ahead of smaller + // powers for the same variable name (but variable name still takes precedence). This ensures + // cases like x^2y+y^2x are sorted in that order. + Comparator.comparing(Variable::getName).thenComparingReversed(Variable::getPower) +} + +private val POLYNOMIAL_TERM_COMPARATOR: Comparator by lazy { + // First, sort by all variable names to ensure xy is placed ahead of xz. Then, sort by variable + // powers in order of the variables (such that x^2y is ranked higher thank xy). Finally, sort by + // the coefficient to ensure equality through the comparator works correctly (though in practice + // like terms should always be combined). Note the specific reversing happening here. It's done in + // this way so that sorted set bigger/smaller list is reversed (which matches expectations since + // larger terms should appear earlier in the results). This is implementing an ordering similar to + // https://en.wikipedia.org/wiki/Polynomial#Definition, except for multi-variable functions (where + // variables of higher degree are preferred over lower degree by lexicographical order of variable + // names). + Comparator.comparing>( + { term -> term.variableList.toSortedSet(POLYNOMIAL_VARIABLE_COMPARATOR) }, + POLYNOMIAL_VARIABLE_COMPARATOR.reversed().toSetComparator() + ).reversed().thenComparing(Term::getCoefficient, REAL_COMPARATOR) +} /** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 +fun Polynomial.isSingleTerm(): Boolean = termList.size == 1 + +fun Polynomial.isApproximatelyZero(): Boolean = + termList.all { it.coefficient.isApproximatelyZero() } // Zero polynomials only have 0 coefs. + /** * Returns the first term coefficient from this polynomial. This corresponds to the whole value of * the polynomial iff isConstant() returns true, otherwise this value isn't useful. @@ -17,6 +47,10 @@ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCoun */ fun Polynomial.getConstant(): Real = getTerm(0).coefficient +// Return the highest power to represent the degree of the polynomial. Reference: +// https://www.varsitytutors.com/algebra_1-help/how-to-find-the-degree-of-a-polynomial. +fun Polynomial.getDegree(): Int = getLeadingTerm().highestDegree() + fun Polynomial.toPlainText(): String { return termList.map { it.toPlainText() }.reduce { acc, termAnswerStr -> if (termAnswerStr.startsWith("-")) { @@ -49,3 +83,276 @@ private fun Term.toPlainText(): String { private fun Variable.toPlainText(): String { return if (power > 1) "$name^$power" else name } + +fun Polynomial.combineLikeTerms(): Polynomial { + // The following algorithm is expected to grow in space by O(N*M) and in time by O(N*m*log(m)) + // where N is the total number of terms, M is the total number of variables, and m is the largest + // single count of variables among all terms (this is assuming constant-time insertion for the + // underlying hashtable). + val newTerms = termList.groupBy { + it.variableList.sortedWith(POLYNOMIAL_VARIABLE_COMPARATOR) + }.mapValues { (_, coefficientTerms) -> + coefficientTerms.map { it.coefficient } + }.mapNotNull { (variables, coefficients) -> + // Combine like terms by summing their coefficients. + val newCoefficient = coefficients.reduce(Real::plus) + return@mapNotNull if (!newCoefficient.isApproximatelyZero()) { + Term.newBuilder().apply { + coefficient = newCoefficient + + // Remove variables with zero powers (since they evaluate to '1'). + addAllVariable(variables.filter { variable -> variable.power != 0 }) + }.build() + } else null // Zero terms should be removed. + } + return Polynomial.newBuilder().apply { + addAllTerm(newTerms) + }.build().ensureAtLeastConstant() +} + +fun Polynomial.removeUnnecessaryVariables(): Polynomial { + return Polynomial.newBuilder().apply { + addAllTerm( + this@removeUnnecessaryVariables.termList.filter { term -> + !term.coefficient.isApproximatelyZero() + } + ) + }.build().ensureAtLeastConstant() +} + +fun Polynomial.sort(): Polynomial = Polynomial.newBuilder().apply { + addAllTerm(this@sort.termList.sortedWith(POLYNOMIAL_TERM_COMPARATOR)) +}.build() + +operator fun Polynomial.unaryMinus(): Polynomial { + // Negating a polynomial just requires flipping the signs on all coefficients. + return toBuilder() + .clearTerm() + .addAllTerm(termList.map { it.toBuilder().setCoefficient(-it.coefficient).build() }) + .build() +} + +operator fun Polynomial.plus(rhs: Polynomial): Polynomial { + // Adding two polynomials just requires combining their terms lists (taking into account combining + // common terms). + return Polynomial.newBuilder().apply { + addAllTerm(this@plus.termList + rhs.termList) + }.build().combineLikeTerms().removeUnnecessaryVariables() +} + +operator fun Polynomial.minus(rhs: Polynomial): Polynomial { + // a - b = a + -b + return this + -rhs +} + +operator fun Polynomial.times(rhs: Polynomial): Polynomial { + // Polynomial multiplication is simply multiplying each term in one by each term in the other. + val crossMultipliedTerms = termList.flatMap { leftTerm -> + rhs.termList.map { rightTerm -> leftTerm * rightTerm } + } + + // Treat each multiplied term as a unique polynomial, then add them together (so that like terms + // can be properly combined). + return crossMultipliedTerms.map { createSingleTermPolynomial(it) }.reduce(Polynomial::plus) +} + +operator fun Polynomial.div(rhs: Polynomial): Polynomial? { + // See https://en.wikipedia.org/wiki/Polynomial_long_division#Pseudocode for reference. + if (rhs.isApproximatelyZero()) { + return null // Dividing by zero is invalid and thus cannot yield a polynomial. + } + + var quotient = createConstantPolynomial(ZERO) + var remainder = this + val leadingDivisorTerm = rhs.getLeadingTerm() + val divisorVariable = leadingDivisorTerm.highestDegreeVariable() + val divisorVariableName = divisorVariable?.name + val divisorDegree = leadingDivisorTerm.highestDegree() + while (!remainder.isApproximatelyZero() && remainder.getDegree() >= divisorDegree) { + // Attempt to divide the leading terms (this may fail). Note that the leading term should always + // be based on the divisor variable being used (otherwise subsequent division steps will be + // inconsistent and potentially fail to resolve). + val newTerm = + remainder.getLeadingTerm(matchedVariable = divisorVariableName) / leadingDivisorTerm + ?: return null + quotient += newTerm.toPolynomial() + remainder -= newTerm.toPolynomial() * rhs + } + return when { + remainder.isApproximatelyZero() -> quotient // Exact division (i.e. with no remainder). + remainder.isConstant() && rhs.isConstant() -> { + // Remainder is a constant term. + val remainingTerm = remainder.getConstant() / rhs.getConstant() + quotient + createConstantPolynomial(remainingTerm) + } + else -> null // Remainder is a polynomial, so the division failed. + } +} + +fun Polynomial.pow(exp: Polynomial): Polynomial? { + // Polynomial exponentiation is only supported if the right side is a constant polynomial, + // otherwise the result cannot be a polynomial (though could still be compared to another + // expression by utilizing sampling techniques). + return if (exp.isConstant()) pow(exp.getConstant()) else null +} + +fun createConstantPolynomial(constant: Real): Polynomial = + createSingleTermPolynomial(Term.newBuilder().setCoefficient(constant).build()) + +fun createSingleTermPolynomial(term: Term): Polynomial = + Polynomial.newBuilder().apply { addTerm(term) }.build() + +private fun Polynomial.pow(exp: Int): Polynomial { + // Anything raised to the power of 0 is 1. + if (exp == 0) return createConstantPolynomial(ONE) + if (exp == 1) return this + var newValue = this + for (i in 1 until exp) newValue *= this + return newValue +} + +private fun Polynomial.pow(rational: Fraction): Polynomial? { + // Polynomials with addition require factoring. + return if (isSingleTerm()) { + termList.first().pow(rational)?.toPolynomial() + } else null +} + +private fun Polynomial.pow(exp: Real): Polynomial? { + val shouldBeInverted = exp.isNegative() + val positivePower = if (shouldBeInverted) -exp else exp + val exponentiation = when { + // Constant polynomials can be raised by any constant. + isConstant() -> createConstantPolynomial(getConstant().pow(positivePower)) + + // Polynomials can only be raised to positive integers (or zero). + exp.isWholeNumber() -> exp.asWholeNumber()?.let { pow(it) } + + // Polynomials can potentially be raised by a fractional power. + exp.isRational() -> pow(exp.rational) + + // All other cases require factoring will definitely not compute to polynomials (such as + // irrational exponents). + else -> null + } + return if (shouldBeInverted) { + val onePolynomial = createConstantPolynomial(ONE) + // Note that this division is guaranteed to fail if the exponentiation result is a polynomial. + // Future implementations may leverage root-finding algorithms to factor for integer inverse + // powers (such as square root, cubic root, etc.). Non-integer inverse powers will require + // sampling. + exponentiation?.let { onePolynomial / it } + } else exponentiation +} + +private operator fun Term.times(rhs: Term): Term { + // The coefficients are always multiplied. + val combinedCoefficient = coefficient * rhs.coefficient + + // Next, create a combined list of new variables. + val combinedVariables = variableList + rhs.variableList + + // Simplify the variables by combining the exponents of like variables. Start with a map of 0 + // powers, then add in the powers of each variable and collect the final list of unique terms. + val variableNamesMap = mutableMapOf() + combinedVariables.forEach { + variableNamesMap.compute(it.name) { _, power -> + if (power != null) power + it.power else it.power + } + } + val newVariableList = variableNamesMap.map { (name, power) -> + Variable.newBuilder().setName(name).setPower(power).build() + } + + return Term.newBuilder() + .setCoefficient(combinedCoefficient) + .addAllVariable(newVariableList) + .build() +} + +private operator fun Term.div(rhs: Term): Term? { + val dividendPowerMap = variableList.toPowerMap() + val divisorPowerMap = rhs.variableList.toPowerMap() + + // If any variables are present in the divisor and not the dividend, this division won't work + // effectively. + if (!dividendPowerMap.keys.containsAll(divisorPowerMap.keys)) return null + + // Division is simply subtracting the powers of terms in the divisor from those in the dividend. + val quotientPowerMap = dividendPowerMap.mapValues { (name, power) -> + power - divisorPowerMap.getOrDefault(name, defaultValue = 0) + } + + // If there are any negative powers, the divisor can't effectively divide this value. + if (quotientPowerMap.values.any { it < 0 }) return null + + // Remove variables with powers of 0 since those have been fully divided. Also, divide the + // coefficients to finish the division. + return Term.newBuilder() + .setCoefficient(coefficient / rhs.coefficient) + .addAllVariable(quotientPowerMap.filter { (_, power) -> power > 0 }.toVariableList()) + .build() +} + +private fun Term.pow(rational: Fraction): Term? { + // Raising an exponent by an exponent just requires multiplying the two together. + val newVariablePowers = variableList.map { variable -> + variable.power.toWholeNumberFraction() * rational + } + + // If any powers are not whole numbers then the rational is likely representing a root and the + // term in question is not rootable to that degree. + if (newVariablePowers.any { !it.isOnlyWholeNumber() }) return null + + return Term.newBuilder().apply { + coefficient = this@pow.coefficient + addAllVariable( + this@pow.variableList.zip(newVariablePowers).map { (variable, newPower) -> + variable.toBuilder().apply { + power = newPower.toWholeNumber() + }.build() + } + ) + }.build() +} + +private fun Polynomial.ensureAtLeastConstant(): Polynomial { + return if (termCount == 0) { + Polynomial.newBuilder().apply { + addTerm( + Term.newBuilder().apply { + coefficient = ZERO + }.build() + ) + }.build() + } else this +} + +private fun Polynomial.getLeadingTerm(matchedVariable: String? = null): Term { + // Return the leading term. Reference: https://undergroundmathematics.org/glossary/leading-term. + return termList.filter { term -> + matchedVariable?.let { variableName -> + term.variableList.any { it.name == variableName } + } ?: true + }.reduce { maxTerm, term -> + val maxTermDegree = maxTerm.highestDegree() + val termDegree = term.highestDegree() + return@reduce if (termDegree > maxTermDegree) term else maxTerm + } +} + +private fun Term.highestDegreeVariable(): Variable? = variableList.maxByOrNull(Variable::getPower) + +private fun Term.highestDegree(): Int = highestDegreeVariable()?.power ?: 0 + +private fun Term.toPolynomial(): Polynomial { + return Polynomial.newBuilder().addTerm(this).build() +} + +private fun List.toPowerMap(): Map { + return associateBy({ it.name }, { it.power }) +} + +private fun Map.toVariableList(): List { + return map { (name, power) -> Variable.newBuilder().setName(name).setPower(power).build() } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index cbe17f0dc92..4359fee66aa 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -8,12 +8,37 @@ import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import kotlin.math.pow +val ZERO: Real by lazy { + Real.newBuilder().apply { integer = 0 }.build() +} + +val ONE: Real by lazy { + Real.newBuilder().apply { integer = 1 }.build() +} + +val ONE_HALF: Real by lazy { + Real.newBuilder().apply { + rational = Fraction.newBuilder().apply { + numerator = 1 + denominator = 2 + }.build() + }.build() +} + val REAL_COMPARATOR: Comparator by lazy { Comparator.comparing(Real::toDouble) } fun Real.isRational(): Boolean = realTypeCase == RATIONAL fun Real.isInteger(): Boolean = realTypeCase == INTEGER +fun Real.isWholeNumber(): Boolean { + return when (realTypeCase) { + RATIONAL -> rational.isOnlyWholeNumber() + INTEGER -> true + IRRATIONAL, REALTYPE_NOT_SET, null -> false + } +} + fun Real.isNegative(): Boolean = when (realTypeCase) { RATIONAL -> rational.isNegative IRRATIONAL -> irrational < 0 @@ -21,6 +46,12 @@ fun Real.isNegative(): Boolean = when (realTypeCase) { REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") } +fun Real.isApproximatelyEqualTo(value: Double): Boolean { + return toDouble().approximatelyEquals(value) +} + +fun Real.isApproximatelyZero(): Boolean = isApproximatelyEqualTo(0.0) + fun Real.toDouble(): Double { return when (realTypeCase) { RATIONAL -> rational.toDouble() @@ -30,6 +61,15 @@ fun Real.toDouble(): Double { } } +fun Real.asWholeNumber(): Int? { + return when (realTypeCase) { + RATIONAL -> if (rational.isOnlyWholeNumber()) rational.toWholeNumber() else null + INTEGER -> integer + IRRATIONAL -> null + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + } +} + fun Real.toPlainText(): String = when (realTypeCase) { // Note that the rational part is first converted to an improper fraction since mixed fractions // can't be expressed as a single coefficient in typical polynomial syntax). @@ -39,10 +79,6 @@ fun Real.toPlainText(): String = when (realTypeCase) { REALTYPE_NOT_SET, null -> "" } -fun Real.isApproximatelyEqualTo(value: Double): Boolean { - return toDouble().approximatelyEquals(value) -} - operator fun Real.unaryMinus(): Real { return when (realTypeCase) { RATIONAL -> recompute { it.setRational(-rational) } diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index fcce3805364..f5784442a09 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -81,6 +81,24 @@ oppia_android_test( ], ) +oppia_android_test( + name = "ExpressionToPolynomialTest", + srcs = ["ExpressionToPolynomialTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.ExpressionToPolynomialTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:parser", + ], +) + oppia_android_test( name = "MathExpressionParserTest", srcs = ["MathExpressionParserTest.kt"], diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt new file mode 100644 index 00000000000..f8163f88c3c --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt @@ -0,0 +1,945 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ExpressionToPolynomialTest { + // TODO: add high-level checks for the three types, but don't test in detail since there are + // separate suites. Also, document the separate suites' existence in this suites's KDoc. + + @Test + fun testPolynomials() { + // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). + + val poly1 = parseNumericExpressionSuccessfully("1").toPolynomial() + assertThat(poly1).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly1).isConstantThat().isIntegerThat().isEqualTo(1) + + val poly13 = parseNumericExpressionSuccessfully("1-1").toPolynomial() + assertThat(poly13).evaluatesToPlainTextThat().isEqualTo("0") + assertThat(poly13).isConstantThat().isIntegerThat().isEqualTo(0) + + val poly2 = parseNumericExpressionSuccessfully("3 + 4 * 2 / (1 - 5) ^ 2").toPolynomial() + assertThat(poly2).evaluatesToPlainTextThat().isEqualTo("7/2") + assertThat(poly2).isConstantThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(3) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + + val poly3 = + parseAlgebraicExpressionSuccessfullyWithAllErrors("133+3.14*x/(11-15)^2").toPolynomial() + assertThat(poly3).evaluatesToPlainTextThat().isEqualTo("0.19625x + 133") + assertThat(poly3).hasTermCountThat().isEqualTo(2) + assertThat(poly3).term(0).hasCoefficientThat().isIrrationalThat().isWithin(1e-5).of(0.19625) + assertThat(poly3).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly3).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly3).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly3).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(133) + assertThat(poly3).term(1).hasVariableCountThat().isEqualTo(0) + + val poly4 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2").toPolynomial() + assertThat(poly4).evaluatesToPlainTextThat().isEqualTo("x^2") + assertThat(poly4).hasTermCountThat().isEqualTo(1) + assertThat(poly4).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly4).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly4).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly4).term(0).variable(0).hasPowerThat().isEqualTo(2) + + val poly5 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy+x").toPolynomial() + assertThat(poly5).evaluatesToPlainTextThat().isEqualTo("xy + x") + assertThat(poly5).hasTermCountThat().isEqualTo(2) + assertThat(poly5).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly5).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly5).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly5).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly5).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly5).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly5).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly5).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly5).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly5).term(1).variable(0).hasPowerThat().isEqualTo(1) + + val poly6 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2x").toPolynomial() + assertThat(poly6).evaluatesToPlainTextThat().isEqualTo("2x") + assertThat(poly6).hasTermCountThat().isEqualTo(1) + assertThat(poly6).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly6).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) + assertThat(poly6).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly6).term(0).variable(0).hasPowerThat().isEqualTo(1) + + val poly30 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x+2").toPolynomial() + assertThat(poly30).evaluatesToPlainTextThat().isEqualTo("x + 2") + assertThat(poly30).hasTermCountThat().isEqualTo(2) + assertThat(poly30).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly30).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(0) + } + + val poly29 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2-3*x-10").toPolynomial() + assertThat(poly29).evaluatesToPlainTextThat().isEqualTo("x^2 - 3x - 10") + assertThat(poly29).hasTermCountThat().isEqualTo(3) + assertThat(poly29).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly29).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly29).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-10) + hasVariableCountThat().isEqualTo(0) + } + + val poly31 = parseAlgebraicExpressionSuccessfullyWithAllErrors("4*(x+2)").toPolynomial() + assertThat(poly31).evaluatesToPlainTextThat().isEqualTo("4x + 8") + assertThat(poly31).hasTermCountThat().isEqualTo(2) + assertThat(poly31).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(4) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly31).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(8) + hasVariableCountThat().isEqualTo(0) + } + + val poly7 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xy^2z^3").toPolynomial() + assertThat(poly7).evaluatesToPlainTextThat().isEqualTo("2xy^2z^3") + assertThat(poly7).hasTermCountThat().isEqualTo(1) + assertThat(poly7).term(0).hasVariableCountThat().isEqualTo(3) + assertThat(poly7).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) + assertThat(poly7).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly7).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly7).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly7).term(0).variable(1).hasPowerThat().isEqualTo(2) + assertThat(poly7).term(0).variable(2).hasNameThat().isEqualTo("z") + assertThat(poly7).term(0).variable(2).hasPowerThat().isEqualTo(3) + + // Show that 7+xy+yz-3-xz-yz+3xy-4 combines into 4xy-xz (the eliminated terms should be gone). + val poly8 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy+yz-xz-yz+3xy").toPolynomial() + assertThat(poly8).evaluatesToPlainTextThat().isEqualTo("4xy - xz") + assertThat(poly8).hasTermCountThat().isEqualTo(2) + assertThat(poly8).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly8).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(4) + assertThat(poly8).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly8).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly8).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly8).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly8).term(1).hasVariableCountThat().isEqualTo(2) + assertThat(poly8).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(-1) + assertThat(poly8).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly8).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly8).term(1).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly8).term(1).variable(1).hasPowerThat().isEqualTo(1) + + // x+2x should become 3x since like terms are combined. + val poly9 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x+2x").toPolynomial() + assertThat(poly9).evaluatesToPlainTextThat().isEqualTo("3x") + assertThat(poly9).hasTermCountThat().isEqualTo(1) + assertThat(poly9).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly9).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(3) + assertThat(poly9).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly9).term(0).variable(0).hasPowerThat().isEqualTo(1) + + // xx^2 should become x^3 since like terms are combined. + val poly10 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xx^2").toPolynomial() + assertThat(poly10).evaluatesToPlainTextThat().isEqualTo("x^3") + assertThat(poly10).hasTermCountThat().isEqualTo(1) + assertThat(poly10).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly10).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly10).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly10).term(0).variable(0).hasPowerThat().isEqualTo(3) + + // No terms in this polynomial should be combined. + val poly11 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2+x+1").toPolynomial() + assertThat(poly11).evaluatesToPlainTextThat().isEqualTo("x^2 + x + 1") + assertThat(poly11).hasTermCountThat().isEqualTo(3) + assertThat(poly11).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly11).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly11).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly11).term(0).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly11).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly11).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly11).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly11).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly11).term(2).hasVariableCountThat().isEqualTo(0) + assertThat(poly11).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + + // No terms in this polynomial should be combined. + val poly12 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2 + x^2y").toPolynomial() + assertThat(poly12).evaluatesToPlainTextThat().isEqualTo("x^2y + x^2") + assertThat(poly12).hasTermCountThat().isEqualTo(2) + assertThat(poly12).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly12).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly12).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly12).term(0).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly12).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly12).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly12).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly12).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly12).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly12).term(1).variable(0).hasPowerThat().isEqualTo(2) + + // Ordering tests. Verify that ordering matches + // https://en.wikipedia.org/wiki/Polynomial#Definition (where multiple variables are sorted + // lexicographically). + + // The order of the terms in this polynomial should be reversed. + val poly14 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x+x^2+x^3").toPolynomial() + assertThat(poly14).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") + assertThat(poly14).hasTermCountThat().isEqualTo(4) + assertThat(poly14).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly14).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly14).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly14).term(0).variable(0).hasPowerThat().isEqualTo(3) + assertThat(poly14).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly14).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly14).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly14).term(1).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly14).term(2).hasVariableCountThat().isEqualTo(1) + assertThat(poly14).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly14).term(2).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly14).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly14).term(3).hasVariableCountThat().isEqualTo(0) + assertThat(poly14).term(3).hasCoefficientThat().isIntegerThat().isEqualTo(1) + + // The order of the terms in this polynomial should be preserved. + val poly15 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^3+x^2+x+1").toPolynomial() + assertThat(poly15).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") + assertThat(poly15).hasTermCountThat().isEqualTo(4) + assertThat(poly15).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly15).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly15).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly15).term(0).variable(0).hasPowerThat().isEqualTo(3) + assertThat(poly15).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly15).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly15).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly15).term(1).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly15).term(2).hasVariableCountThat().isEqualTo(1) + assertThat(poly15).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly15).term(2).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly15).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly15).term(3).hasVariableCountThat().isEqualTo(0) + assertThat(poly15).term(3).hasCoefficientThat().isIntegerThat().isEqualTo(1) + + // The order of the terms in this polynomial should be reversed. + val poly16 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy+xz+yz").toPolynomial() + assertThat(poly16).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") + assertThat(poly16).hasTermCountThat().isEqualTo(3) + assertThat(poly16).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly16).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly16).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly16).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly16).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(1).hasVariableCountThat().isEqualTo(2) + assertThat(poly16).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly16).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly16).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(1).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly16).term(1).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(2).hasVariableCountThat().isEqualTo(2) + assertThat(poly16).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly16).term(2).variable(0).hasNameThat().isEqualTo("y") + assertThat(poly16).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(2).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly16).term(2).variable(1).hasPowerThat().isEqualTo(1) + + // The order of the terms in this polynomial should be preserved. + val poly17 = parseAlgebraicExpressionSuccessfullyWithAllErrors("yz+xz+xy").toPolynomial() + assertThat(poly17).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") + assertThat(poly17).hasTermCountThat().isEqualTo(3) + assertThat(poly17).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly17).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly17).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly17).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly17).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(1).hasVariableCountThat().isEqualTo(2) + assertThat(poly17).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly17).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly17).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(1).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly17).term(1).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(2).hasVariableCountThat().isEqualTo(2) + assertThat(poly17).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly17).term(2).variable(0).hasNameThat().isEqualTo("y") + assertThat(poly17).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(2).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly17).term(2).variable(1).hasPowerThat().isEqualTo(1) + + val poly18 = + parseAlgebraicExpressionSuccessfullyWithAllErrors("3+x+y+xy+x^2y+xy^2+x^2y^2").toPolynomial() + assertThat(poly18).evaluatesToPlainTextThat().isEqualTo("x^2y^2 + x^2y + xy^2 + xy + x + y + 3") + assertThat(poly18).hasTermCountThat().isEqualTo(7) + assertThat(poly18).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly18).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly18).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(4).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(5).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(6).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(0) + } + + // Ensure variables of coefficient and power of 0 are removed. + val poly22 = parseAlgebraicExpressionSuccessfullyWithAllErrors("0x").toPolynomial() + assertThat(poly22).evaluatesToPlainTextThat().isEqualTo("0") + assertThat(poly22).hasTermCountThat().isEqualTo(1) + assertThat(poly22).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(0) + hasVariableCountThat().isEqualTo(0) + } + + val poly23 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x-x").toPolynomial() + assertThat(poly23).evaluatesToPlainTextThat().isEqualTo("0") + assertThat(poly23).hasTermCountThat().isEqualTo(1) + assertThat(poly23).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(0) + hasVariableCountThat().isEqualTo(0) + } + + val poly24 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^0").toPolynomial() + assertThat(poly24).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly24).hasTermCountThat().isEqualTo(1) + assertThat(poly24).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + + val poly25 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x/x").toPolynomial() + assertThat(poly25).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly25).hasTermCountThat().isEqualTo(1) + assertThat(poly25).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + + val poly26 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^(2-2)").toPolynomial() + assertThat(poly26).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly26).hasTermCountThat().isEqualTo(1) + assertThat(poly26).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + + val poly28 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x+1)/2").toPolynomial() + assertThat(poly28).evaluatesToPlainTextThat().isEqualTo("(1/2)x + 1/2") + assertThat(poly28).hasTermCountThat().isEqualTo(2) + assertThat(poly28).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly28).term(1).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(0) + } + + // Ensure like terms are combined after polynomial multiplication. + val poly20 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-5)(x+2)").toPolynomial() + assertThat(poly20).evaluatesToPlainTextThat().isEqualTo("x^2 - 3x - 10") + assertThat(poly20).hasTermCountThat().isEqualTo(3) + assertThat(poly20).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly20).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly20).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-10) + hasVariableCountThat().isEqualTo(0) + } + + val poly21 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(1+x)^3").toPolynomial() + assertThat(poly21).evaluatesToPlainTextThat().isEqualTo("x^3 + 3x^2 + 3x + 1") + assertThat(poly21).hasTermCountThat().isEqualTo(4) + assertThat(poly21).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + assertThat(poly21).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly21).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly21).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + + val poly27 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2*y^2 + 2").toPolynomial() + assertThat(poly27).evaluatesToPlainTextThat().isEqualTo("x^2y^2 + 2") + assertThat(poly27).hasTermCountThat().isEqualTo(2) + assertThat(poly27).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly27).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(0) + } + + val poly32 = + parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^2-3*x-10)*(x+2)").toPolynomial() + assertThat(poly32).evaluatesToPlainTextThat().isEqualTo("x^3 - x^2 - 16x - 20") + assertThat(poly32).hasTermCountThat().isEqualTo(4) + assertThat(poly32).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + assertThat(poly32).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly32).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-16) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly32).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-20) + hasVariableCountThat().isEqualTo(0) + } + + val poly33 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-y)^3").toPolynomial() + assertThat(poly33).evaluatesToPlainTextThat().isEqualTo("x^3 - 3x^2y + 3xy^2 - y^3") + assertThat(poly33).hasTermCountThat().isEqualTo(4) + assertThat(poly33).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + assertThat(poly33).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly33).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly33).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(3) + } + } + + // Ensure polynomial division works. + val poly19 = + parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^2-3*x-10)/(x+2)").toPolynomial() + assertThat(poly19).evaluatesToPlainTextThat().isEqualTo("x - 5") + assertThat(poly19).hasTermCountThat().isEqualTo(2) + assertThat(poly19).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly19).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-5) + hasVariableCountThat().isEqualTo(0) + } + + val poly35 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(xy-5y)/y").toPolynomial() + assertThat(poly35).evaluatesToPlainTextThat().isEqualTo("x - 5") + assertThat(poly35).hasTermCountThat().isEqualTo(2) + assertThat(poly35).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly35).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-5) + hasVariableCountThat().isEqualTo(0) + } + + val poly36 = + parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^2-2xy+y^2)/(x-y)").toPolynomial() + assertThat(poly36).evaluatesToPlainTextThat().isEqualTo("x - y") + assertThat(poly36).hasTermCountThat().isEqualTo(2) + assertThat(poly36).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly36).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + + // Example from https://www.kristakingmath.com/blog/predator-prey-systems-ghtcp-5e2r4-427ab. + val poly37 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^3-y^3)/(x-y)").toPolynomial() + assertThat(poly37).evaluatesToPlainTextThat().isEqualTo("x^2 + xy + y^2") + assertThat(poly37).hasTermCountThat().isEqualTo(3) + assertThat(poly37).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly37).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly37).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + + // Multi-variable & more complex division. + val poly34 = + parseAlgebraicExpressionSuccessfullyWithAllErrors( + "(x^3-3x^2y+3xy^2-y^3)/(x-y)^2" + ).toPolynomial() + assertThat(poly34).evaluatesToPlainTextThat().isEqualTo("x - y") + assertThat(poly34).hasTermCountThat().isEqualTo(2) + assertThat(poly34).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly34).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + + val poly38 = parseNumericExpressionSuccessfully("2^-4").toPolynomial() + assertThat(poly38).evaluatesToPlainTextThat().isEqualTo("1/16") + assertThat(poly38).hasTermCountThat().isEqualTo(1) + assertThat(poly38).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(16) + } + hasVariableCountThat().isEqualTo(0) + } + + val poly39 = parseNumericExpressionSuccessfully("2^(3-6)").toPolynomial() + assertThat(poly39).evaluatesToPlainTextThat().isEqualTo("1/8") + assertThat(poly39).hasTermCountThat().isEqualTo(1) + assertThat(poly39).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(8) + } + hasVariableCountThat().isEqualTo(0) + } + + // x^-3 is not a valid polynomial (since polynomials can't have negative powers). + val poly40 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^(3-6)").toPolynomial() + assertThat(poly40).isNotValidPolynomial() + + // 2^x is not a polynomial. + val poly41 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("2^x").toPolynomial() + assertThat(poly41).isNotValidPolynomial() + + // 1/x is not a polynomial. + val poly42 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("1/x").toPolynomial() + assertThat(poly42).isNotValidPolynomial() + + val poly43 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x/2").toPolynomial() + assertThat(poly43).evaluatesToPlainTextThat().isEqualTo("(1/2)x") + assertThat(poly43).hasTermCountThat().isEqualTo(1) + assertThat(poly43).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + + val poly44 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-3)/2").toPolynomial() + assertThat(poly44).evaluatesToPlainTextThat().isEqualTo("(1/2)x - 3/2") + assertThat(poly44).hasTermCountThat().isEqualTo(2) + assertThat(poly44).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly44).term(1).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isTrue() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(3) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(0) + } + + val poly45 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-1)(x+1)").toPolynomial() + assertThat(poly45).evaluatesToPlainTextThat().isEqualTo("x^2 - 1") + assertThat(poly45).hasTermCountThat().isEqualTo(2) + assertThat(poly45).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly45).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + + // √x is not a polynomial. + val poly46 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(x)").toPolynomial() + assertThat(poly46).isNotValidPolynomial() + + val poly47 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(x^2)").toPolynomial() + assertThat(poly47).evaluatesToPlainTextThat().isEqualTo("x") + assertThat(poly47).hasTermCountThat().isEqualTo(1) + assertThat(poly47).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + + val poly51 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(x^2y^2)").toPolynomial() + assertThat(poly51).evaluatesToPlainTextThat().isEqualTo("xy") + assertThat(poly51).hasTermCountThat().isEqualTo(1) + assertThat(poly51).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + + // A limitation in the current polynomial conversion is that sqrt(x) will fail due to it not + // have any polynomial representation. + val poly48 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√x^2").toPolynomial() + assertThat(poly48).isNotValidPolynomial() + + // √(x^2+2) may evaluate to a polynomial, but it requires factoring (which isn't yet supported). + val poly50 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(x^2+2)").toPolynomial() + assertThat(poly50).isNotValidPolynomial() + + // Division by zero is undefined, so a polynomial can't be constructed. + val poly49 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("(x+2)/0").toPolynomial() + assertThat(poly49).isNotValidPolynomial() + + val poly52 = parsePolynomialFromNumericExpression("1") + val poly53 = parsePolynomialFromNumericExpression("0") + assertThat(poly52).isNotEqualTo(poly53) + + val poly54 = parsePolynomialFromNumericExpression("1+2") + val poly55 = parsePolynomialFromNumericExpression("3") + assertThat(poly54).isEqualTo(poly55) + + val poly56 = parsePolynomialFromNumericExpression("1-2") + val poly57 = parsePolynomialFromNumericExpression("-1") + assertThat(poly56).isEqualTo(poly57) + + val poly58 = parsePolynomialFromNumericExpression("2*3") + val poly59 = parsePolynomialFromNumericExpression("6") + assertThat(poly58).isEqualTo(poly59) + + val poly60 = parsePolynomialFromNumericExpression("2^3") + val poly61 = parsePolynomialFromNumericExpression("8") + assertThat(poly60).isEqualTo(poly61) + + val poly62 = parsePolynomialFromAlgebraicExpression("1+x") + val poly63 = parsePolynomialFromAlgebraicExpression("x+1") + assertThat(poly62).isEqualTo(poly63) + + val poly64 = parsePolynomialFromAlgebraicExpression("y+x") + val poly65 = parsePolynomialFromAlgebraicExpression("x+y") + assertThat(poly64).isEqualTo(poly65) + + val poly66 = parsePolynomialFromAlgebraicExpression("(x+1)^2") + val poly67 = parsePolynomialFromAlgebraicExpression("x^2+2x+1") + assertThat(poly66).isEqualTo(poly67) + + val poly68 = parsePolynomialFromAlgebraicExpression("(x+1)/2") + val poly69 = parsePolynomialFromAlgebraicExpression("x/2+(1/2)") + assertThat(poly68).isEqualTo(poly69) + + val poly70 = parsePolynomialFromAlgebraicExpression("x*2") + val poly71 = parsePolynomialFromAlgebraicExpression("2x") + assertThat(poly70).isEqualTo(poly71) + + val poly72 = parsePolynomialFromAlgebraicExpression("x(x+1)") + val poly73 = parsePolynomialFromAlgebraicExpression("x^2+x") + assertThat(poly72).isEqualTo(poly73) + } + + private fun parsePolynomialFromNumericExpression(expression: String) = + parseNumericExpressionSuccessfully(expression).toPolynomial() + + private fun parsePolynomialFromAlgebraicExpression(expression: String) = + parseAlgebraicExpressionSuccessfullyWithAllErrors(expression).toPolynomial() + + private companion object { + // TODO: fix helper API. + + private fun parseNumericExpressionSuccessfully(expression: String): MathExpression { + val result = parseNumericExpressionWithAllErrors(expression) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionWithAllErrors( + expression: String + ): MathParsingResult { + return MathExpressionParser.parseNumericExpression(expression, ErrorCheckingMode.ALL_ERRORS) + } + + private fun parseAlgebraicExpressionSuccessfullyWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) + } + } +} From 8623a2141314efb73bb0a96afa810908a3ed5f54 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 14:45:43 -0800 Subject: [PATCH 113/289] Remove unneeded test. This test is now in a new test suite per the previous merge commit. --- .../util/math/MathExpressionParserTest.kt | 1551 ----------------- 1 file changed, 1551 deletions(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index d5f9f5de3f2..44bdb8fed85 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -714,1557 +714,6 @@ class MathExpressionParserTest { ) } - @Test - fun testToComparableOperation() { - // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). - - val exp1 = parseNumericExpressionSuccessfully("1") - assertThat(exp1.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - - val exp2 = parseNumericExpressionSuccessfully("-1") - assertThat(exp2.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - - val exp3 = parseNumericExpressionSuccessfully("1+3+4") - assertThat(exp3.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - - val exp4 = parseNumericExpressionSuccessfully("-1-2-3") - assertThat(exp4.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(2) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - - val exp5 = parseNumericExpressionSuccessfully("1+2-3") - assertThat(exp5.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(2) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - - val exp6 = parseNumericExpressionSuccessfully("2*3*4") - assertThat(exp6.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - - val exp7 = parseNumericExpressionSuccessfully("1-2*3") - assertThat(exp7.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - } - } - - val exp8 = parseNumericExpressionSuccessfully("2*3-4") - assertThat(exp8.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - - val exp9 = parseNumericExpressionSuccessfully("1+2*3-4+8*7*6-9") - assertThat(exp9.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(6) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(8) - } - } - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(3) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(4) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(9) - } - } - } - } - - val exp10 = parseNumericExpressionSuccessfully("2/3/4") - assertThat(exp10.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - - val exp11 = parseNumericExpressionWithoutOptionalErrors("2^3^4") - assertThat(exp11.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - } - - val exp12 = parseNumericExpressionSuccessfully("1+2/3+3") - assertThat(exp12.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - - val exp13 = parseNumericExpressionSuccessfully("1+(2/3)+3") - assertThat(exp13.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - - val exp14 = parseNumericExpressionSuccessfully("1+2^3+3") - assertThat(exp14.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - - val exp15 = parseNumericExpressionSuccessfully("1+(2^3)+3") - assertThat(exp15.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - - val exp16 = parseNumericExpressionSuccessfully("2*3/4*7") - assertThat(exp16.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(4) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - - val exp17 = parseNumericExpressionSuccessfully("2*(3/4)*7") - assertThat(exp17.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(4) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - - val exp18 = parseNumericExpressionSuccessfully("-3*sqrt(2)") - assertThat(exp18.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - squareRootWithArgument { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - - val exp19 = parseNumericExpressionSuccessfully("1+(2+(3+(4+5)))") - assertThat(exp19.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(4) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - - val exp20 = parseNumericExpressionSuccessfully("2*(3*(4*(5*6)))") - assertThat(exp20.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - index(4) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(6) - } - } - } - } - - val exp21 = parseAlgebraicExpressionSuccessfully("x") - assertThat(exp21.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - - val exp22 = parseAlgebraicExpressionSuccessfully("-x") - assertThat(exp22.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - - val exp23 = parseAlgebraicExpressionSuccessfully("1+x+y") - assertThat(exp23.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - - val exp24 = parseAlgebraicExpressionSuccessfully("-1-x-y") - assertThat(exp24.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - - val exp25 = parseAlgebraicExpressionSuccessfully("1+x-y") - assertThat(exp25.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - - val exp26 = parseAlgebraicExpressionSuccessfully("2xy") - assertThat(exp26.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - - val exp27 = parseAlgebraicExpressionSuccessfully("1-xy") - assertThat(exp27.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - } - } - - val exp28 = parseAlgebraicExpressionSuccessfully("xy-4") - assertThat(exp28.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - - val exp29 = parseAlgebraicExpressionSuccessfully("1+xy-4+yz-9") - assertThat(exp29.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(3) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(4) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(9) - } - } - } - } - - val exp30 = parseAlgebraicExpressionSuccessfully("2/x/y") - assertThat(exp30.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - - val exp31 = parseAlgebraicExpressionWithoutOptionalErrors("x^3^4") - assertThat(exp31.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - } - - val exp32 = parseAlgebraicExpressionSuccessfully("1+x/y+z") - assertThat(exp32.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - - val exp33 = parseAlgebraicExpressionSuccessfully("1+(x/y)+z") - assertThat(exp33.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - - val exp34 = parseAlgebraicExpressionSuccessfully("1+x^3+y") - assertThat(exp34.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - - val exp35 = parseAlgebraicExpressionSuccessfully("1+(x^3)+y") - assertThat(exp35.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - - val exp36 = parseAlgebraicExpressionSuccessfully("2*x/y*z") - assertThat(exp36.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(4) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - - val exp37 = parseAlgebraicExpressionSuccessfully("2*(x/y)*z") - assertThat(exp37.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(4) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - - val exp38 = parseAlgebraicExpressionSuccessfully("-2*sqrt(x)") - assertThat(exp38.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - squareRootWithArgument { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - - val exp39 = parseAlgebraicExpressionSuccessfully("1+(x+(3+(z+y)))") - assertThat(exp39.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - index(4) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - - val exp40 = parseAlgebraicExpressionSuccessfully("2*(x*(4*(zy)))") - assertThat(exp40.toComparableOperationList()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - index(4) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - - // Equality tests: - val list1 = createComparableOperationListFromNumericExpression("(1+2)+3") - val list2 = createComparableOperationListFromNumericExpression("1+(2+3)") - assertThat(list1).isEqualTo(list2) - - val list3 = createComparableOperationListFromNumericExpression("1+2+3") - val list4 = createComparableOperationListFromNumericExpression("3+2+1") - assertThat(list3).isEqualTo(list4) - - val list5 = createComparableOperationListFromNumericExpression("1-2-3") - val list6 = createComparableOperationListFromNumericExpression("-3 + -2 + 1") - assertThat(list5).isEqualTo(list6) - - val list7 = createComparableOperationListFromNumericExpression("1-2-3") - val list8 = createComparableOperationListFromNumericExpression("-3-2+1") - assertThat(list7).isEqualTo(list8) - - val list9 = createComparableOperationListFromNumericExpression("1-2-3") - val list10 = createComparableOperationListFromNumericExpression("-3-2+1") - assertThat(list9).isEqualTo(list10) - - val list11 = createComparableOperationListFromNumericExpression("1-2-3") - val list12 = createComparableOperationListFromNumericExpression("3-2-1") - assertThat(list11).isNotEqualTo(list12) - - val list13 = createComparableOperationListFromNumericExpression("2*3*4") - val list14 = createComparableOperationListFromNumericExpression("4*3*2") - assertThat(list13).isEqualTo(list14) - - val list15 = createComparableOperationListFromNumericExpression("2*(3/4)") - val list16 = createComparableOperationListFromNumericExpression("3/4*2") - assertThat(list15).isEqualTo(list16) - - val list17 = createComparableOperationListFromNumericExpression("2*3/4") - val list18 = createComparableOperationListFromNumericExpression("3/4*2") - assertThat(list17).isEqualTo(list18) - - val list45 = createComparableOperationListFromNumericExpression("2*3/4") - val list46 = createComparableOperationListFromNumericExpression("2*3*4") - assertThat(list45).isNotEqualTo(list46) - - val list19 = createComparableOperationListFromNumericExpression("2*3/4") - val list20 = createComparableOperationListFromNumericExpression("2*4/3") - assertThat(list19).isNotEqualTo(list20) - - val list21 = createComparableOperationListFromNumericExpression("2*3/4*7") - val list22 = createComparableOperationListFromNumericExpression("3/4*7*2") - assertThat(list21).isEqualTo(list22) - - val list23 = createComparableOperationListFromNumericExpression("2*3/4*7") - val list24 = createComparableOperationListFromNumericExpression("7*(3*2/4)") - assertThat(list23).isEqualTo(list24) - - val list25 = createComparableOperationListFromNumericExpression("2*3/4*7") - val list26 = createComparableOperationListFromNumericExpression("7*3*2/4") - assertThat(list25).isEqualTo(list26) - - val list27 = createComparableOperationListFromNumericExpression("-2*3") - val list28 = createComparableOperationListFromNumericExpression("3*-2") - assertThat(list27).isEqualTo(list28) - - val list29 = createComparableOperationListFromNumericExpression("2^3") - val list30 = createComparableOperationListFromNumericExpression("3^2") - assertThat(list29).isNotEqualTo(list30) - - val list31 = createComparableOperationListFromNumericExpression("-(1+2)") - val list32 = createComparableOperationListFromNumericExpression("-1+2") - assertThat(list31).isNotEqualTo(list32) - - val list33 = createComparableOperationListFromNumericExpression("-(1+2)") - val list34 = createComparableOperationListFromNumericExpression("-1-2") - assertThat(list33).isNotEqualTo(list34) - - val list35 = createComparableOperationListFromAlgebraicExpression("x(x+1)") - val list36 = createComparableOperationListFromAlgebraicExpression("(1+x)x") - assertThat(list35).isEqualTo(list36) - - val list37 = createComparableOperationListFromAlgebraicExpression("x(x+1)") - val list38 = createComparableOperationListFromAlgebraicExpression("x^2+x") - assertThat(list37).isNotEqualTo(list38) - - val list39 = createComparableOperationListFromAlgebraicExpression("x^2*sqrt(x)") - val list40 = createComparableOperationListFromAlgebraicExpression("x") - assertThat(list39).isNotEqualTo(list40) - - val list41 = createComparableOperationListFromAlgebraicExpression("xyz") - val list42 = createComparableOperationListFromAlgebraicExpression("zyx") - assertThat(list41).isEqualTo(list42) - - val list43 = createComparableOperationListFromAlgebraicExpression("1+xy-2") - val list44 = createComparableOperationListFromAlgebraicExpression("-2+1+yx") - assertThat(list43).isEqualTo(list44) - - // TODO: add tests for comparator/sorting & negation simplification? - } - @Test fun testPolynomials() { // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). From 961b3d05c7c3af64c5fae3a08e9611b976ee8ae0 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 15:25:05 -0800 Subject: [PATCH 114/289] Add NumericExpressionInput classifiers. This doesn't hook them up to the application or tests yet (that will happen in a later PR). --- domain/BUILD.bazel | 1 + ...nteractionObjectTypeExtractorRepository.kt | 1 + ...putIsEquivalentToRuleClassifierProvider.kt | 58 +++++++++++++++++++ ...atchesExactlyWithRuleClassifierProvider.kt | 49 ++++++++++++++++ ...vialManipulationsRuleClassifierProvider.kt | 51 ++++++++++++++++ .../NumericExpressionInputModule.kt | 36 ++++++++++++ .../util/InteractionObjectExtensions.kt | 2 + model/src/main/proto/interaction_object.proto | 1 + 8 files changed, 199 insertions(+) create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index cf4b8098c8e..fed48085058 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -125,6 +125,7 @@ kt_android_library( "//utility/src/main/java/org/oppia/android/util/logging:event_logger", "//utility/src/main/java/org/oppia/android/util/logging:log_uploader", "//utility/src/main/java/org/oppia/android/util/math:extensions", + "//utility/src/main/java/org/oppia/android/util/math:parser", "//utility/src/main/java/org/oppia/android/util/networking:network_connection_util", "//utility/src/main/java/org/oppia/android/util/profile:directory_management_util", ], diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/InteractionObjectTypeExtractorRepository.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/InteractionObjectTypeExtractorRepository.kt index 9b72e5dea69..48bec5b8f5c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/InteractionObjectTypeExtractorRepository.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/InteractionObjectTypeExtractorRepository.kt @@ -76,6 +76,7 @@ class InteractionObjectTypeExtractorRepository @Inject constructor() { createMapping(InteractionObject::getListOfSetsOfTranslatableHtmlContentIds) ObjectTypeCase.TRANSLATABLE_SET_OF_NORMALIZED_STRING -> createMapping(InteractionObject::getTranslatableSetOfNormalizedString) + ObjectTypeCase.MATH_EXPRESSION -> createMapping(InteractionObject::getMathExpression) ObjectTypeCase.OBJECTTYPE_NOT_SET -> createMapping { error("Invalid object type") } } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt new file mode 100644 index 00000000000..ecf89663802 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -0,0 +1,58 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.Polynomial +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.GenericRuleClassifier +import org.oppia.android.domain.classify.rules.RuleClassifierProvider +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.toPolynomial +import javax.inject.Inject + +class NumericExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + writtenTranslationContext: WrittenTranslationContext + ): Boolean { + val answerExpression = parsePolynomial(answer) ?: return false + val inputExpression = parsePolynomial(input) ?: return false + return answerExpression == inputExpression + } + + private fun parsePolynomial(rawExpression: String): Polynomial? { + return when (val expResult = MathExpressionParser.parseNumericExpression(rawExpression)) { + is MathParsingResult.Success -> { + expResult.result.toPolynomial().also { + if (it == null) { + consoleLogger.w( + "NumericExpEquivalent", "Expression is not a supported polynomial: $rawExpression." + ) + } + } + } + is MathParsingResult.Failure -> { + consoleLogger.e( + "NumericExpEquivalent", + "Encountered expression that failed parsing. Expression: $rawExpression." + + " Failure: ${expResult.error}." + ) + null + } + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt new file mode 100644 index 00000000000..9ce52a59519 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -0,0 +1,49 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.GenericRuleClassifier +import org.oppia.android.domain.classify.rules.RuleClassifierProvider +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import javax.inject.Inject + +class NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + writtenTranslationContext: WrittenTranslationContext + ): Boolean { + val answerExpression = parseNumericExpression(answer) ?: return false + val inputExpression = parseNumericExpression(input) ?: return false + return answerExpression == inputExpression + } + + private fun parseNumericExpression(rawExpression: String): MathExpression? { + return when (val expResult = MathExpressionParser.parseNumericExpression(rawExpression)) { + is MathParsingResult.Success -> expResult.result + is MathParsingResult.Failure -> { + consoleLogger.e( + "NumericExpMatchesExact", + "Encountered expression that failed parsing. Expression: $rawExpression." + + " Failure: ${expResult.error}." + ) + null + } + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt new file mode 100644 index 00000000000..d1ea260e948 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -0,0 +1,51 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.GenericRuleClassifier +import org.oppia.android.domain.classify.rules.RuleClassifierProvider +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.toComparableOperationList +import javax.inject.Inject + +class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider +@Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + writtenTranslationContext: WrittenTranslationContext + ): Boolean { + val answerExpression = parseComparableOperationList(answer) ?: return false + val inputExpression = parseComparableOperationList(input) ?: return false + return answerExpression == inputExpression + } + + private fun parseComparableOperationList(rawExpression: String): ComparableOperationList? { + return when (val expResult = MathExpressionParser.parseNumericExpression(rawExpression)) { + is MathParsingResult.Success -> expResult.result.toComparableOperationList() + is MathParsingResult.Failure -> { + consoleLogger.e( + "NumericExpTrivialManips", + "Encountered expression that failed parsing. Expression: $rawExpression." + + " Failure: ${expResult.error}." + ) + null + } + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt new file mode 100644 index 00000000000..cb010da48de --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt @@ -0,0 +1,36 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import dagger.multibindings.StringKey +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.FractionInputRules + +/** Module that binds rule classifiers corresponding to the numeric expression input interaction. */ +@Module +class NumericExpressionInputModule { + @Provides + @IntoMap + @StringKey("MatchesExactlyWith") + @FractionInputRules + internal fun provideNumericExpressionInputMatchesExactlyWithRuleClassifierProvider( + classifierProvider: NumericExpressionInputMatchesExactlyWithRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() + + @Provides + @IntoMap + @StringKey("MatchesUpToTrivialManipulations") + @FractionInputRules + internal fun provideNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( + classifierProvider: NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() + + @Provides + @IntoMap + @StringKey("IsEquivalentTo") + @FractionInputRules + internal fun provideNumericExpressionInputIsEquivalentToRuleClassifier( + classifierProvider: NumericExpressionInputIsEquivalentToRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() +} diff --git a/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt b/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt index 32f9123e852..8ad6c07ecd1 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt +++ b/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt @@ -9,6 +9,7 @@ import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.FRACTION import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.IMAGE_WITH_REGIONS import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.LIST_OF_SETS_OF_HTML_STRING import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.LIST_OF_SETS_OF_TRANSLATABLE_HTML_CONTENT_IDS +import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.MATH_EXPRESSION import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.NON_NEGATIVE_INT import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.NORMALIZED_STRING import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.NUMBER_WITH_UNITS @@ -53,6 +54,7 @@ fun InteractionObject.toAnswerString(): String { LIST_OF_SETS_OF_TRANSLATABLE_HTML_CONTENT_IDS -> listOfSetsOfTranslatableHtmlContentIds.toAnswerString() TRANSLATABLE_SET_OF_NORMALIZED_STRING -> translatableSetOfNormalizedString.toAnswerString() + MATH_EXPRESSION -> mathExpression OBJECTTYPE_NOT_SET -> "" // The default InteractionObject should be an empty string. } } diff --git a/model/src/main/proto/interaction_object.proto b/model/src/main/proto/interaction_object.proto index bb6154f9255..1e7b95ed7ef 100644 --- a/model/src/main/proto/interaction_object.proto +++ b/model/src/main/proto/interaction_object.proto @@ -29,6 +29,7 @@ message InteractionObject { TranslatableHtmlContentId translatable_html_content_id = 14; SetOfTranslatableHtmlContentIds set_of_translatable_html_content_ids = 15; ListOfSetsOfTranslatableHtmlContentIds list_of_sets_of_translatable_html_content_ids = 16; + string math_expression = 17; } } From 535ae2a8108f49dd8cb68e2443550b21d9e4be1f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 17:35:27 -0800 Subject: [PATCH 115/289] Introduce ClassificationContext. This refactor introduces support for passing customization arguments down to classifiers (which is needed for algebraic expression input). --- .../AnswerClassificationController.kt | 7 ++- .../domain/classify/ClassificationContext.kt | 10 +++ .../android/domain/classify/RuleClassifier.kt | 3 +- .../classify/rules/GenericRuleClassifier.kt | 48 +++++++------- ...asElementXAtPositionYClassifierProvider.kt | 4 +- ...lementXBeforeElementYClassifierProvider.kt | 4 +- ...nputIsEqualToOrderingClassifierProvider.kt | 4 +- ...emAtIncorrectPositionClassifierProvider.kt | 4 +- ...enominatorEqualToRuleClassifierProvider.kt | 4 +- ...artExactlyEqualToRuleClassifierProvider.kt | 4 +- ...ntegerPartEqualToRuleClassifierProvider.kt | 4 +- ...sNoFractionalPartRuleClassifierProvider.kt | 4 +- ...sNumeratorEqualToRuleClassifierProvider.kt | 4 +- ...AndInSimplestFormRuleClassifierProvider.kt | 4 +- ...putIsEquivalentToRuleClassifierProvider.kt | 4 +- ...tIsExactlyEqualToRuleClassifierProvider.kt | 4 +- ...nputIsGreaterThanRuleClassifierProvider.kt | 4 +- ...onInputIsLessThanRuleClassifierProvider.kt | 4 +- ...ckInputIsInRegionRuleClassifierProvider.kt | 4 +- ...tainsAtLeastOneOfRuleClassifierProvider.kt | 4 +- ...ntainAtLeastOneOfRuleClassifierProvider.kt | 4 +- ...ectionInputEqualsRuleClassifierProvider.kt | 4 +- ...tIsProperSubsetOfRuleClassifierProvider.kt | 4 +- ...ChoiceInputEqualsRuleClassifierProvider.kt | 4 +- ...ithUnitsIsEqualToRuleClassifierProvider.kt | 4 +- ...itsIsEquivalentToRuleClassifierProvider.kt | 4 +- ...putIsEquivalentToRuleClassifierProvider.kt | 4 +- ...atchesExactlyWithRuleClassifierProvider.kt | 4 +- ...vialManipulationsRuleClassifierProvider.kt | 4 +- ...umericInputEqualsRuleClassifierProvider.kt | 4 +- ...aterThanOrEqualToRuleClassifierProvider.kt | 4 +- ...nputIsGreaterThanRuleClassifierProvider.kt | 4 +- ...nclusivelyBetweenRuleClassifierProvider.kt | 4 +- ...LessThanOrEqualToRuleClassifierProvider.kt | 4 +- ...icInputIsLessThanRuleClassifierProvider.kt | 4 +- ...IsWithinToleranceRuleClassifierProvider.kt | 4 +- .../RatioInputEqualsRuleClassifierProvider.kt | 4 +- ...sNumberOfTermsEqualToClassifierProvider.kt | 4 +- ...ecificTermEqualToRuleClassifierProvider.kt | 4 +- ...InputIsEquivalentRuleClassifierProvider.kt | 4 +- ...TextInputContainsRuleClassifierProvider.kt | 9 ++- .../TextInputEqualsRuleClassifierProvider.kt | 9 ++- ...tInputFuzzyEqualsRuleClassifierProvider.kt | 9 ++- ...xtInputStartsWithRuleClassifierProvider.kt | 9 ++- ...tXAtPositionYRuleClassifierProviderTest.kt | 22 +++---- ...eforeElementYRuleClassifierProviderTest.kt | 20 +++--- ...IsEqualToOrderingClassifierProviderTest.kt | 14 ++--- ...IncorrectPositionClassifierProviderTest.kt | 14 ++--- ...inatorEqualToRuleClassifierProviderTest.kt | 14 ++--- ...xactlyEqualToRuleClassifierProviderTest.kt | 22 +++---- ...erPartEqualToRuleClassifierProviderTest.kt | 44 ++++++------- ...ractionalPartRuleClassifierProviderTest.kt | 18 +++--- ...eratorEqualToRuleClassifierProviderTest.kt | 16 ++--- ...nSimplestFormRuleClassifierProviderTest.kt | 32 +++++----- ...sEquivalentToRuleClassifierProviderTest.kt | 26 ++++---- ...xactlyEqualToRuleClassifierProviderTest.kt | 26 ++++---- ...IsGreaterThanRuleClassifierProviderTest.kt | 54 ++++++++-------- ...putIsLessThanRuleClassifierProviderTest.kt | 54 ++++++++-------- ...putIsInRegionRuleClassifierProviderTest.kt | 12 ++-- ...sAtLeastOneOfRuleClassifierProviderTest.kt | 14 ++--- ...nAtLeastOneOfRuleClassifierProviderTest.kt | 24 +++---- ...onInputEqualsRuleClassifierProviderTest.kt | 20 +++--- ...roperSubsetOfRuleClassifierProviderTest.kt | 26 ++++---- ...ceInputEqualsRuleClassifierProviderTest.kt | 12 ++-- ...nitsIsEqualToRuleClassifierProviderTest.kt | 16 ++--- ...sEquivalentToRuleClassifierProviderTest.kt | 18 +++--- ...icInputEqualsRuleClassifierProviderTest.kt | 20 +++--- ...ThanOrEqualToRuleClassifierProviderTest.kt | 26 ++++---- ...IsGreaterThanRuleClassifierProviderTest.kt | 26 ++++---- ...sivelyBetweenRuleClassifierProviderTest.kt | 38 ++++++------ ...ThanOrEqualToRuleClassifierProviderTest.kt | 30 ++++----- ...putIsLessThanRuleClassifierProviderTest.kt | 30 ++++----- ...thinToleranceRuleClassifierProviderTest.kt | 62 +++++++++---------- ...ioInputEqualsRuleClassifierProviderTest.kt | 12 ++-- ...berOfTermsEqualToClassifierProviderTest.kt | 10 +-- ...icTermEqualToRuleClassifierProviderTest.kt | 32 +++++----- ...tIsEquivalentRuleClassifierProviderTest.kt | 18 +++--- ...InputContainsRuleClassifierProviderTest.kt | 48 +++++++------- ...xtInputEqualsRuleClassifierProviderTest.kt | 40 +++++++----- ...utFuzzyEqualsRuleClassifierProviderTest.kt | 58 ++++++++++------- ...putStartsWithRuleClassifierProviderTest.kt | 52 +++++++++------- 81 files changed, 658 insertions(+), 610 deletions(-) create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt diff --git a/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt b/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt index 7281ff50485..55eb6023584 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt @@ -36,13 +36,14 @@ class AnswerClassificationController @Inject constructor( "expected one of: ${interactionClassifiers.keys}" } // TODO(#207): Add support for additional classification types. + interaction.customizationArgsMap return classifyAnswer( answer, interaction.answerGroupsList, interaction.defaultOutcome, interactionClassifier, interaction.id, - writtenTranslationContext + ClassificationContext(writtenTranslationContext, interaction.customizationArgsMap) ) } @@ -54,7 +55,7 @@ class AnswerClassificationController @Inject constructor( defaultOutcome: Outcome, interactionClassifier: InteractionClassifier, interactionId: String, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): ClassificationResult { for (answerGroup in answerGroups) { for (ruleSpec in answerGroup.ruleSpecsList) { @@ -65,7 +66,7 @@ class AnswerClassificationController @Inject constructor( " has: ${interactionClassifier.getRuleTypes()}" } try { - if (ruleClassifier.matches(answer, ruleSpec.inputMap, writtenTranslationContext)) { + if (ruleClassifier.matches(answer, ruleSpec.inputMap, classificationContext)) { // Explicit classification matched. return if (!answerGroup.hasTaggedSkillMisconception()) { ClassificationResult.OutcomeOnly(answerGroup.outcome) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt b/domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt new file mode 100644 index 00000000000..01415330ce2 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt @@ -0,0 +1,10 @@ +package org.oppia.android.domain.classify + +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.WrittenTranslationContext + +data class ClassificationContext( + val writtenTranslationContext: WrittenTranslationContext = + WrittenTranslationContext.getDefaultInstance(), + val customizationArgs: Map = mapOf() +) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/RuleClassifier.kt b/domain/src/main/java/org/oppia/android/domain/classify/RuleClassifier.kt index f4fba3fc301..973b97e4fb2 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/RuleClassifier.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/RuleClassifier.kt @@ -1,7 +1,6 @@ package org.oppia.android.domain.classify import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext /** An answer classifier for a specific interaction rule. */ interface RuleClassifier { @@ -12,6 +11,6 @@ interface RuleClassifier { fun matches( answer: InteractionObject, inputs: Map, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/GenericRuleClassifier.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/GenericRuleClassifier.kt index 95f81ee1e9b..ed6efda5b4a 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/GenericRuleClassifier.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/GenericRuleClassifier.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import javax.inject.Inject @@ -15,15 +15,15 @@ import javax.inject.Inject */ // TODO(#1580): Re-restrict access using Bazel visibilities class GenericRuleClassifier constructor( - val expectedAnswerObjectType: InteractionObject.ObjectTypeCase, - val orderedExpectedParameterTypes: LinkedHashMap< + private val expectedAnswerObjectType: InteractionObject.ObjectTypeCase, + private val orderedExpectedParameterTypes: LinkedHashMap< String, InteractionObject.ObjectTypeCase>, - val matcherDelegate: MatcherDelegate + private val matcherDelegate: MatcherDelegate ) : RuleClassifier { override fun matches( answer: InteractionObject, inputs: Map, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { check(answer.objectTypeCase == expectedAnswerObjectType) { "Expected answer to be of type ${expectedAnswerObjectType.name} " + @@ -34,7 +34,7 @@ class GenericRuleClassifier constructor( .map { (parameterName, expectedObjectType) -> retrieveInputObject(parameterName, expectedObjectType, inputs) } - return matcherDelegate.matches(answer, parameterInputs, writtenTranslationContext) + return matcherDelegate.matches(answer, parameterInputs, classificationContext) } private fun retrieveInputObject( @@ -58,7 +58,7 @@ class GenericRuleClassifier constructor( * Returns whether the validated and extracted answer matches the expectations per the * specification of this classifier. */ - fun matches(answer: T, writtenTranslationContext: WrittenTranslationContext): Boolean + fun matches(answer: T, classificationContext: ClassificationContext): Boolean } interface SingleInputMatcher { @@ -66,7 +66,7 @@ class GenericRuleClassifier constructor( * Returns whether the validated and extracted answer matches the single validated and extracted * input parameter per the specification of this classifier. */ - fun matches(answer: T, input: T, writtenTranslationContext: WrittenTranslationContext): Boolean + fun matches(answer: T, input: T, classificationContext: ClassificationContext): Boolean } interface MultiTypeSingleInputMatcher { @@ -74,11 +74,7 @@ class GenericRuleClassifier constructor( * Returns whether the validated and extracted answer matches the single validated and extracted * input parameter per the specification of this classifier. */ - fun matches( - answer: AT, - input: IT, - writtenTranslationContext: WrittenTranslationContext - ): Boolean + fun matches(answer: AT, input: IT, classificationContext: ClassificationContext): Boolean } interface MultiTypeDoubleInputMatcher { @@ -90,7 +86,7 @@ class GenericRuleClassifier constructor( answer: AT, firstInput: ITF, secondInput: ITS, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean } @@ -103,7 +99,7 @@ class GenericRuleClassifier constructor( answer: T, firstInput: T, secondInput: T, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean } @@ -112,7 +108,7 @@ class GenericRuleClassifier constructor( abstract fun matches( answer: InteractionObject, inputs: List, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean class NoInputMatcherDelegate( @@ -122,10 +118,10 @@ class GenericRuleClassifier constructor( override fun matches( answer: InteractionObject, inputs: List, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { check(inputs.isEmpty()) - return matcher.matches(extractObject(answer), writtenTranslationContext) + return matcher.matches(extractObject(answer), classificationContext) } } @@ -136,11 +132,11 @@ class GenericRuleClassifier constructor( override fun matches( answer: InteractionObject, inputs: List, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { check(inputs.size == 1) return matcher.matches( - extractObject(answer), extractObject(inputs.first()), writtenTranslationContext + extractObject(answer), extractObject(inputs.first()), classificationContext ) } } @@ -153,11 +149,11 @@ class GenericRuleClassifier constructor( override fun matches( answer: InteractionObject, inputs: List, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { check(inputs.size == 1) return matcher.matches( - extractAnswerObject(answer), extractInputObject(inputs.first()), writtenTranslationContext + extractAnswerObject(answer), extractInputObject(inputs.first()), classificationContext ) } } @@ -169,14 +165,14 @@ class GenericRuleClassifier constructor( override fun matches( answer: InteractionObject, inputs: List, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { check(inputs.size == 2) return matcher.matches( extractObject(answer), extractObject(inputs[0]), extractObject(inputs[1]), - writtenTranslationContext + classificationContext ) } } @@ -190,14 +186,14 @@ class GenericRuleClassifier constructor( override fun matches( answer: InteractionObject, inputs: List, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { check(inputs.size == 2) return matcher.matches( extractAnswerObject(answer), extractFirstParamObject(inputs[0]), extractSecondParamObject(inputs[1]), - writtenTranslationContext + classificationContext ) } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYClassifierProvider.kt index 72affc7d851..b6388896d97 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYClassifierProvider.kt @@ -5,7 +5,7 @@ import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.NON_NEGATIVE import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.TRANSLATABLE_HTML_CONTENT_ID import org.oppia.android.app.model.ListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.app.model.TranslatableHtmlContentId -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -45,7 +45,7 @@ class DragDropSortInputHasElementXAtPositionYClassifierProvider @Inject construc answer: ListOfContentIdSets1, firstInput: ContentId1, secondInput: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { // Note that the '1' returned here is to have consistency with the web platform: matched indexes // start at 1 rather than 0 to make the indexes more human friendly. diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYClassifierProvider.kt index 41fa56e9bcd..2c79702d5c9 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYClassifierProvider.kt @@ -4,7 +4,7 @@ import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.LIST_OF_SETS import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.TRANSLATABLE_HTML_CONTENT_ID import org.oppia.android.app.model.ListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.app.model.TranslatableHtmlContentId -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -44,7 +44,7 @@ class DragDropSortInputHasElementXBeforeElementYClassifierProvider @Inject const answer: ListOfContentIdSets2, firstInput: ContentId2, secondInput: ContentId2, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val answerSets = answer.contentIdListsList.map { it.getContentIdSet() } return answerSets.indexOfFirst { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProvider.kt index 18b8335d51c..60fdbd5d66d 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProvider.kt @@ -3,7 +3,7 @@ package org.oppia.android.domain.classify.rules.dragAndDropSortInput import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.LIST_OF_SETS_OF_TRANSLATABLE_HTML_CONTENT_IDS import org.oppia.android.app.model.ListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -34,7 +34,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProvider @Inject constructor( override fun matches( answer: ListOfSetsOfTranslatableHtmlContentIds, input: ListOfSetsOfTranslatableHtmlContentIds, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = areListOfSetsOfHtmlStringsEqual(answer, input) /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProvider.kt index acf28424dfe..64dcae58082 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.dragAndDropSortInput import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.LIST_OF_SETS_OF_TRANSLATABLE_HTML_CONTENT_IDS import org.oppia.android.app.model.ListOfSetsOfTranslatableHtmlContentIds -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -33,7 +33,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier override fun matches( answer: ListOfSetsOfTranslatableHtmlContentIds, input: ListOfSetsOfTranslatableHtmlContentIds, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val answerStringSets = answer.contentIdListsList val inputStringSets = input.contentIdListsList diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProvider.kt index 7467770aaee..1331c2d000c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProvider @Inject construct override fun matches( answer: Fraction, input: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.denominator == input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider.kt index 5daf835e119..e8e75bce46d 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider override fun matches( answer: Fraction, input: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.numerator == input.numerator && answer.denominator == input.denominator } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProvider.kt index cdf8425ec19..81b953e27ca 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProvider @Inject construct override fun matches( answer: Fraction, input: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.wholeNumber == input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProvider.kt index 30b63ee8020..d47a4ea43cd 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,7 +28,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProvider @Inject constructor override fun matches( answer: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.numerator == 0 } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProvider.kt index ac3da4c4d15..fa56ec057b5 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProvider @Inject constructor override fun matches( answer: Fraction, input: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.numerator == input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt index f9498f7d965..9cb2dca6aac 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -34,7 +34,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider override fun matches( answer: Fraction, input: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.toDouble().approximatelyEquals(input.toDouble()) && answer == input.toSimplestForm() diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt index e2c42f7ec67..8d745d5e3df 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -32,7 +32,7 @@ class FractionInputIsEquivalentToRuleClassifierProvider @Inject constructor( override fun matches( answer: Fraction, input: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.toDouble().approximatelyEquals(input.toDouble()) } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProvider.kt index 628824681c2..eaf41d88008 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -30,7 +30,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProvider @Inject constructor( override fun matches( answer: Fraction, input: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer == input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt index 89d83f1e3d6..05d4936965c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class FractionInputIsGreaterThanRuleClassifierProvider @Inject constructor( override fun matches( answer: Fraction, input: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.toDouble() > input.toDouble() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt index 02d4b9766c9..b4e0fde2e20 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class FractionInputIsLessThanRuleClassifierProvider @Inject constructor( override fun matches( answer: Fraction, input: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.toDouble() < input.toDouble() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProvider.kt index 3504fdaa68e..4f13484722f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.imageClickInput import org.oppia.android.app.model.ClickOnImage import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class ImageClickInputIsInRegionRuleClassifierProvider @Inject constructor( override fun matches( answer: ClickOnImage, input: String, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.clickedRegionsList.indexOf(input) != -1 } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider.kt index f434bdd263c..f84e30b9b32 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.itemselectioninput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -32,6 +32,6 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider @Inject const override fun matches( answer: SetOfTranslatableHtmlContentIds, input: SetOfTranslatableHtmlContentIds, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.getContentIdSet().intersect(input.getContentIdSet()).isNotEmpty() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider.kt index 5dbe1330c19..e3d306c9afc 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.itemselectioninput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -33,6 +33,6 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider override fun matches( answer: SetOfTranslatableHtmlContentIds, input: SetOfTranslatableHtmlContentIds, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.getContentIdSet().intersect(input.getContentIdSet()).isEmpty() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProvider.kt index c447df24546..4227906dbaf 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.itemselectioninput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -32,6 +32,6 @@ class ItemSelectionInputEqualsRuleClassifierProvider @Inject constructor( override fun matches( answer: SetOfTranslatableHtmlContentIds, input: SetOfTranslatableHtmlContentIds, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.getContentIdSet() == input.getContentIdSet() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProvider.kt index ba14381872a..c7a35286733 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.itemselectioninput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -32,7 +32,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProvider @Inject construct override fun matches( answer: SetOfTranslatableHtmlContentIds, input: SetOfTranslatableHtmlContentIds, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val answerSet = answer.getContentIdSet() val inputSet = input.getContentIdSet() diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProvider.kt index 8814f50fee4..09254a65821 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.multiplechoiceinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -29,6 +29,6 @@ class MultipleChoiceInputEqualsRuleClassifierProvider @Inject constructor( override fun matches( answer: Int, input: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer == input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt index 9a225cc41ca..004168c1d1c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt @@ -3,7 +3,7 @@ package org.oppia.android.domain.classify.rules.numberwithunits import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.NumberWithUnits -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -33,7 +33,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProvider @Inject constructor( override fun matches( answer: NumberWithUnits, input: NumberWithUnits, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { // The number types must match. if (answer.numberTypeCase != input.numberTypeCase) { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt index e94fc9191e7..d3a8bfae04a 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.numberwithunits import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.NumberWithUnits -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -33,7 +33,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProvider @Inject constructor( override fun matches( answer: NumberWithUnits, input: NumberWithUnits, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { // Units must match, but in different orders is fine. if (answer.unitList.toSet() != input.unitList.toSet()) { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt index ecf89663802..6af1e922742 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.Polynomial -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -27,7 +27,7 @@ class NumericExpressionInputIsEquivalentToRuleClassifierProvider @Inject constru override fun matches( answer: String, input: String, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val answerExpression = parsePolynomial(answer) ?: return false val inputExpression = parsePolynomial(input) ?: return false diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index 9ce52a59519..4769660a921 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.MathExpression -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -26,7 +26,7 @@ class NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject con override fun matches( answer: String, input: String, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val answerExpression = parseNumericExpression(answer) ?: return false val inputExpression = parseNumericExpression(input) ?: return false diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index d1ea260e948..60793efa071 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput import org.oppia.android.app.model.ComparableOperationList import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,7 +28,7 @@ class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvide override fun matches( answer: String, input: String, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val answerExpression = parseComparableOperationList(answer) ?: return false val inputExpression = parseComparableOperationList(input) ?: return false diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt index 2c7a6dc5212..7d0f45b793c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -29,6 +29,6 @@ class NumericInputEqualsRuleClassifierProvider @Inject constructor( override fun matches( answer: Double, input: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = input.approximatelyEquals(answer) } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProvider.kt index 09021d836e7..dabdb9c762e 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,6 +28,6 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProvider @Inject construct override fun matches( answer: Double, input: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer >= input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProvider.kt index e62a916aa48..41b5532ca13 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,6 +28,6 @@ class NumericInputIsGreaterThanRuleClassifierProvider @Inject constructor( override fun matches( answer: Double, input: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer > input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProvider.kt index 052e5ebc60d..2c8e664402a 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,6 +31,6 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProvider @Inject constructor answer: Double, firstInput: Double, secondInput: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer in firstInput..secondInput } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProvider.kt index 034b8f61f40..3dc1ddce52c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,6 +28,6 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProvider @Inject constructor( override fun matches( answer: Double, input: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer <= input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProvider.kt index 26d56bcaa86..3d5e9cf53ff 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,6 +28,6 @@ class NumericInputIsLessThanRuleClassifierProvider @Inject constructor( override fun matches( answer: Double, input: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer < input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProvider.kt index ab830d2e3ff..3b3980587a4 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,6 +31,6 @@ class NumericInputIsWithinToleranceRuleClassifierProvider @Inject constructor( answer: Double, firstInput: Double, secondInput: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer in (firstInput - secondInput)..(firstInput + secondInput) } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProvider.kt index c8fbe57a790..5165c781c02 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.ratioinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.RatioExpression -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,6 +28,6 @@ class RatioInputEqualsRuleClassifierProvider @Inject constructor( override fun matches( answer: RatioExpression, input: RatioExpression, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.ratioComponentList == input.ratioComponentList } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProvider.kt index 0506de932c3..bdf58bd8d1f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.ratioinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.RatioExpression -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -29,6 +29,6 @@ class RatioInputHasNumberOfTermsEqualToClassifierProvider @Inject constructor( override fun matches( answer: RatioExpression, input: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.ratioComponentCount == input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProvider.kt index 749e8bef6a4..75ecb50355f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.ratioinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.RatioExpression -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -33,6 +33,6 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProvider @Inject constructor answer: RatioExpression, firstInput: Int, secondInput: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.ratioComponentList.getOrNull(firstInput - 1) == secondInput } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt index f9b9b2e9df8..c21fcc0944c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.ratioinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.RatioExpression -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -29,6 +29,6 @@ class RatioInputIsEquivalentRuleClassifierProvider @Inject constructor( override fun matches( answer: RatioExpression, input: RatioExpression, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.toSimplestForm() == input.toSimplestForm() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProvider.kt index 76b65756b5b..853b513da31 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.textinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.TranslatableSetOfNormalizedString -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -37,10 +37,13 @@ class TextInputContainsRuleClassifierProvider @Inject constructor( override fun matches( answer: String, input: TranslatableSetOfNormalizedString, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val normalizedAnswer = machineLocale.run { answer.normalizeWhitespace().toMachineLowerCase() } - val inputStringList = translationController.extractStringList(input, writtenTranslationContext) + val inputStringList = + translationController.extractStringList( + input, classificationContext.writtenTranslationContext + ) return inputStringList.any { normalizedAnswer.contains(machineLocale.run { it.normalizeWhitespace().toMachineLowerCase() }) } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProvider.kt index a4b705f05b8..a0c3034d6e7 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.textinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.TranslatableSetOfNormalizedString -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -37,10 +37,13 @@ class TextInputEqualsRuleClassifierProvider @Inject constructor( override fun matches( answer: String, input: TranslatableSetOfNormalizedString, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val normalizedAnswer = answer.normalizeWhitespace() - val inputStringList = translationController.extractStringList(input, writtenTranslationContext) + val inputStringList = + translationController.extractStringList( + input, classificationContext.writtenTranslationContext + ) return inputStringList.any { machineLocale.run { it.normalizeWhitespace().equalsIgnoreCase(normalizedAnswer) } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt index 3c69d469dec..c660a01f133 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.textinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.TranslatableSetOfNormalizedString -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -37,9 +37,12 @@ class TextInputFuzzyEqualsRuleClassifierProvider @Inject constructor( override fun matches( answer: String, input: TranslatableSetOfNormalizedString, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { - val inputStringList = translationController.extractStringList(input, writtenTranslationContext) + val inputStringList = + translationController.extractStringList( + input, classificationContext.writtenTranslationContext + ) return inputStringList.any { hasEditDistanceEqualToOne(it, answer) } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt index a216eabce4c..39c6c7b83d5 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.textinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.TranslatableSetOfNormalizedString -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -37,10 +37,13 @@ class TextInputStartsWithRuleClassifierProvider @Inject constructor( override fun matches( answer: String, input: TranslatableSetOfNormalizedString, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val normalizedAnswer = machineLocale.run { answer.normalizeWhitespace().toMachineLowerCase() } - val inputStringList = translationController.extractStringList(input, writtenTranslationContext) + val inputStringList = + translationController.extractStringList( + input, classificationContext.writtenTranslationContext + ) return inputStringList.any { normalizedAnswer.startsWith( machineLocale.run { it.normalizeWhitespace().toMachineLowerCase() } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest.kt index d73f1a71934..53be477a285 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableHtmlContentId @@ -59,7 +59,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -79,7 +79,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -96,7 +96,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -113,7 +113,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -130,7 +130,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -147,7 +147,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -164,7 +164,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -178,7 +178,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -192,7 +192,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -206,7 +206,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest.kt index 06ea75a0cf3..c706db71f64 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableHtmlContentId @@ -60,7 +60,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -77,7 +77,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -94,7 +94,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -111,7 +111,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -128,7 +128,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -145,7 +145,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -162,7 +162,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -176,7 +176,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -190,7 +190,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProviderTest.kt index f6efcddc2f6..2c65122884d 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -71,7 +71,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProviderTest { isEqualToOrderingClassifierProvider.matches( answer = LIST_OF_SETS_12_3_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -85,7 +85,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProviderTest { isEqualToOrderingClassifierProvider.matches( answer = LIST_OF_SETS_12_3_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -99,7 +99,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProviderTest { isEqualToOrderingClassifierProvider.matches( answer = LIST_OF_SETS_12_3_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -113,7 +113,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProviderTest { isEqualToOrderingClassifierProvider.matches( answer = LIST_OF_SETS_12_3_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -127,7 +127,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProviderTest { isEqualToOrderingClassifierProvider.matches( answer = LIST_OF_SETS_12_3_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -141,7 +141,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProviderTest { isEqualToOrderingClassifierProvider.matches( answer = LIST_OF_SETS_12_3_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProviderTest.kt index af473238da2..d62690db395 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.RuleClassifier @@ -73,7 +73,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier isEqualToOrderingWithOneItemIncorrectClassifier.matches( answer = LIST_OF_SETS_12_4_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -92,7 +92,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier isEqualToOrderingWithOneItemIncorrectClassifier.matches( answer = LIST_OF_SETS_12_4_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -106,7 +106,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier isEqualToOrderingWithOneItemIncorrectClassifier.matches( answer = LIST_OF_SETS_1_4_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -120,7 +120,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier isEqualToOrderingWithOneItemIncorrectClassifier.matches( answer = LIST_OF_SETS_1_4_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -134,7 +134,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier isEqualToOrderingWithOneItemIncorrectClassifier.matches( answer = LIST_OF_SETS_1_4_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -148,7 +148,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier isEqualToOrderingWithOneItemIncorrectClassifier.matches( answer = LIST_OF_SETS_12_4_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProviderTest.kt index 3fa9315aaa0..a4641cc64ad 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -71,7 +71,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProviderTest { denominatorIsEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // This should match because whole numbers have a denominator of 1 by default @@ -86,7 +86,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProviderTest { denominatorIsEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -100,7 +100,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProviderTest { denominatorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -114,7 +114,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProviderTest { denominatorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -128,7 +128,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProviderTest { denominatorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -147,7 +147,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProviderTest { denominatorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest.kt index af55a78b926..56212d736d9 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -99,7 +99,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -113,7 +113,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -127,7 +127,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -141,7 +141,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_321, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // 123 and 321 match because they have the same fractional parts: 0/1. @@ -156,7 +156,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -170,7 +170,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -184,7 +184,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -198,7 +198,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -212,7 +212,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -231,7 +231,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProviderTest.kt index a9e2bf4eda8..1ce8cbc6e94 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -146,7 +146,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -159,7 +159,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_5_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -172,7 +172,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_5_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -185,7 +185,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_3_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -198,7 +198,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_3_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -211,7 +211,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -224,7 +224,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -237,7 +237,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_0_2_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -250,7 +250,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_1_2_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -263,7 +263,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_1_2_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -276,7 +276,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -289,7 +289,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_0_2_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -302,7 +302,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -315,7 +315,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_5_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -328,7 +328,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_5_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -341,7 +341,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_3_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -354,7 +354,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_3_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -366,7 +366,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_1_2_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -379,7 +379,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_1_2_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -394,7 +394,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { .matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -412,7 +412,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { .matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProviderTest.kt index 6d8a25460f8..cb72a6d9b50 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProviderTest.kt @@ -10,7 +10,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.robolectric.annotation.Config @@ -97,7 +97,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -111,7 +111,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -125,7 +125,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -139,7 +139,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_0_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -153,7 +153,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -167,7 +167,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_0_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -181,7 +181,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -195,7 +195,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = FRACTION_VALUE_TEST_20_OVER_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProviderTest.kt index 8b8352c78db..647d37cbdd3 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -79,7 +79,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -93,7 +93,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -107,7 +107,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -121,7 +121,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -135,7 +135,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -149,7 +149,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -168,7 +168,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest.kt index 5210a2162a1..1afbb516319 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -111,7 +111,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -126,7 +126,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -141,7 +141,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -156,7 +156,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -171,7 +171,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -186,7 +186,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // Even if creator does not input simplest form, learner's answer must still be in simplest form @@ -202,7 +202,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -217,7 +217,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -232,7 +232,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -247,7 +247,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -262,7 +262,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -277,7 +277,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -292,7 +292,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -310,7 +310,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -328,7 +328,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProviderTest.kt index 5453058e53b..a61d6a03b41 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -100,7 +100,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -114,7 +114,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -128,7 +128,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_2_OVER_8, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -142,7 +142,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_2_OVER_8, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -156,7 +156,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -170,7 +170,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_6_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -184,7 +184,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_55_1_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -198,7 +198,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_13_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -212,7 +212,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = WHOLE_NUMBER_VALUE_TEST_254, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -226,7 +226,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_55_1_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -240,7 +240,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_6_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -254,7 +254,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_2_OVER_8, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProviderTest.kt index 083294f9ce5..5280da48f8a 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -99,7 +99,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -113,7 +113,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_321, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -127,7 +127,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -141,7 +141,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -155,7 +155,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -169,7 +169,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -183,7 +183,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -197,7 +197,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -211,7 +211,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -225,7 +225,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -239,7 +239,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -258,7 +258,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProviderTest.kt index 63b132c8441..b14d9993e06 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -100,7 +100,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -113,7 +113,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -126,7 +126,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -139,7 +139,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -152,7 +152,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -165,7 +165,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -178,7 +178,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -191,7 +191,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -204,7 +204,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -217,7 +217,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -230,7 +230,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -243,7 +243,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -256,7 +256,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -269,7 +269,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -282,7 +282,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -295,7 +295,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -308,7 +308,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -321,7 +321,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -334,7 +334,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -347,7 +347,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -360,7 +360,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -373,7 +373,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -386,7 +386,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -399,7 +399,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -414,7 +414,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { .matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -432,7 +432,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { .matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProviderTest.kt index 8318da73310..1dda20b3d7c 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -100,7 +100,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -113,7 +113,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -126,7 +126,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -139,7 +139,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -152,7 +152,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -165,7 +165,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -178,7 +178,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -191,7 +191,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -204,7 +204,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -217,7 +217,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -230,7 +230,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -243,7 +243,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -256,7 +256,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -269,7 +269,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -282,7 +282,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -295,7 +295,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -308,7 +308,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -321,7 +321,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -334,7 +334,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -347,7 +347,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -360,7 +360,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -373,7 +373,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -386,7 +386,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -399,7 +399,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -414,7 +414,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { .matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -432,7 +432,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { .matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProviderTest.kt index 7acae52a13b..0694388d0c9 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProviderTest.kt @@ -12,7 +12,7 @@ import org.junit.runner.RunWith import org.oppia.android.app.model.ClickOnImage import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.Point2d -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -51,7 +51,7 @@ class ImageClickInputIsInRegionRuleClassifierProviderTest { isInRegionClassifierProvider.matches( answer = IMAGE_REGION_ABC_POSITION_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -65,7 +65,7 @@ class ImageClickInputIsInRegionRuleClassifierProviderTest { isInRegionClassifierProvider.matches( answer = IMAGE_REGION_ABC_POSITION_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -79,7 +79,7 @@ class ImageClickInputIsInRegionRuleClassifierProviderTest { isInRegionClassifierProvider.matches( answer = IMAGE_REGION_ABC_POSITION_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -93,7 +93,7 @@ class ImageClickInputIsInRegionRuleClassifierProviderTest { isInRegionClassifierProvider.matches( answer = IMAGE_REGION_ABC_POSITION_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -112,7 +112,7 @@ class ImageClickInputIsInRegionRuleClassifierProviderTest { isInRegionClassifierProvider.matches( answer = IMAGE_REGION_ABC_POSITION_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest.kt index e17ba337f4a..e182692d80b 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createSetOfTranslatableHtmlContentIds import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode @@ -52,7 +52,7 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -65,7 +65,7 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -78,7 +78,7 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -91,7 +91,7 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -104,7 +104,7 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -117,7 +117,7 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest.kt index f31abfa389c..99be995d0bc 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createSetOfTranslatableHtmlContentIds import org.oppia.android.testing.assertThrows @@ -55,7 +55,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -68,7 +68,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -81,7 +81,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -94,7 +94,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -107,7 +107,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -120,7 +120,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -133,7 +133,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_EMPTY, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -146,7 +146,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -160,7 +160,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -177,7 +177,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -194,7 +194,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = DIFFERENT_INTERACTION_OBJECT_TYPE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProviderTest.kt index 748a43c7a12..927dcdc5f3f 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createSetOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -54,7 +54,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -68,7 +68,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -82,7 +82,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -96,7 +96,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -110,7 +110,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -124,7 +124,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_MIXED_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -138,7 +138,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -152,7 +152,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -166,7 +166,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest.kt index 5e22f731b95..8997d85c434 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createSetOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createString import org.oppia.android.testing.assertThrows @@ -55,7 +55,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -68,7 +68,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -81,7 +81,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -94,7 +94,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_126, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -107,7 +107,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_16, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -120,7 +120,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_NONE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -133,7 +133,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_NONE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -146,7 +146,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_6, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -159,7 +159,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -173,7 +173,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -190,7 +190,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_INVAILD, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -207,7 +207,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProviderTest.kt index 691b84e99a2..d193f4c795b 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -49,7 +49,7 @@ class MultipleChoiceInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = NON_NEGATIVE_VALUE_TEST_0, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -62,7 +62,7 @@ class MultipleChoiceInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = NON_NEGATIVE_VALUE_TEST_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -76,7 +76,7 @@ class MultipleChoiceInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = NON_NEGATIVE_VALUE_TEST_0, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -93,7 +93,7 @@ class MultipleChoiceInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -110,7 +110,7 @@ class MultipleChoiceInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = NON_NEGATIVE_VALUE_TEST_0, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProviderTest.kt index 4557720a234..048e5574066 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -105,7 +105,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = ANSWER_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -119,7 +119,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = TEST_REAL_INPUT_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -133,7 +133,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = INPUT_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -147,7 +147,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = ANSWER_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -161,7 +161,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = ANSWER_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -175,7 +175,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = TEST_REAL_ANSWER_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -190,7 +190,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = DOUBLE_VALUE_TEST_DIFFERENT_TYPE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProviderTest.kt index 2176ab2837c..d53553b9d79 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -99,7 +99,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = DIFF_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -113,7 +113,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = ANSWER_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -127,7 +127,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = ANSWER_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -141,7 +141,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = DIFF_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -155,7 +155,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = ANSWER_TEST_REAL_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -169,7 +169,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = ANSWER_TEST_REAL_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -183,7 +183,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = DOUBLE_VALUE_TEST_DIFFERENT_TYPE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -202,7 +202,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = ANSWER_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt index 1c7f3c2eb96..b8f7e8a0f9f 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.util.FLOAT_EQUALITY_INTERVAL import org.oppia.android.testing.assertThrows @@ -65,7 +65,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -79,7 +79,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -94,7 +94,7 @@ class NumericInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = FIVE_POINT_ONE_TIMES_FLOAT_EQUALITY_INTERVAL, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -108,7 +108,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -122,7 +122,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -136,7 +136,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -151,7 +151,7 @@ class NumericInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = SIX_TIMES_FLOAT_EQUALITY_INTERVAL, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -165,7 +165,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -182,7 +182,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest.kt index 53aacbb513a..b7298602e46 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -63,7 +63,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -77,7 +77,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -91,7 +91,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -105,7 +105,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -119,7 +119,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -133,7 +133,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -147,7 +147,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -161,7 +161,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -175,7 +175,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -189,7 +189,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -203,7 +203,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -220,7 +220,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProviderTest.kt index 26e38c83922..aa4f78a021d 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -64,7 +64,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { .matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -78,7 +78,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -92,7 +92,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -106,7 +106,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -120,7 +120,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -134,7 +134,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -148,7 +148,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -162,7 +162,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -176,7 +176,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -190,7 +190,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -204,7 +204,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -221,7 +221,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProviderTest.kt index fbe82358b66..504117c488e 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -75,7 +75,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -91,7 +91,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -107,7 +107,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -123,7 +123,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -139,7 +139,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -155,7 +155,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -171,7 +171,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -187,7 +187,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -203,7 +203,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -219,7 +219,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -235,7 +235,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -254,7 +254,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -273,7 +273,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -292,7 +292,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -311,7 +311,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -330,7 +330,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -349,7 +349,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -368,7 +368,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProviderTest.kt index c3894d36ccb..1bcc688f8c3 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -63,7 +63,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -77,7 +77,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -91,7 +91,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -105,7 +105,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -119,7 +119,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -133,7 +133,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -147,7 +147,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -161,7 +161,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -175,7 +175,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -189,7 +189,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -203,7 +203,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -220,7 +220,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -237,7 +237,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -254,7 +254,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProviderTest.kt index 50f1e00fbe7..97d152ce3e4 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -63,7 +63,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -77,7 +77,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -91,7 +91,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -105,7 +105,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -119,7 +119,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -133,7 +133,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -147,7 +147,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -161,7 +161,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -175,7 +175,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -189,7 +189,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -203,7 +203,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -220,7 +220,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -237,7 +237,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_INT_VALUE_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -254,7 +254,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProviderTest.kt index b0a6148d256..d5481c0ac89 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -78,7 +78,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { answer = POSITIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -94,7 +94,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -110,7 +110,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -126,7 +126,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -142,7 +142,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -158,7 +158,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -174,7 +174,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -190,7 +190,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -206,7 +206,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -222,7 +222,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -238,7 +238,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -254,7 +254,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -270,7 +270,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -286,7 +286,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -302,7 +302,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -318,7 +318,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -334,7 +334,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -350,7 +350,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -366,7 +366,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -382,7 +382,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -397,7 +397,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -415,7 +415,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -434,7 +434,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -453,7 +453,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -472,7 +472,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -490,7 +490,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -508,7 +508,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -527,7 +527,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -546,7 +546,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -565,7 +565,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProviderTest.kt index 6e221e3a790..244cb981571 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -62,7 +62,7 @@ class RatioInputEqualsRuleClassifierProviderTest { equalsClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -76,7 +76,7 @@ class RatioInputEqualsRuleClassifierProviderTest { equalsClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -90,7 +90,7 @@ class RatioInputEqualsRuleClassifierProviderTest { equalsClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -104,7 +104,7 @@ class RatioInputEqualsRuleClassifierProviderTest { equalsClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -123,7 +123,7 @@ class RatioInputEqualsRuleClassifierProviderTest { equalsClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProviderTest.kt index 9a38307e9d5..2cbb3b324a4 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -58,7 +58,7 @@ class RatioInputHasNumberOfTermsEqualToClassifierProviderTest { hasNumberOfTermsEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -72,7 +72,7 @@ class RatioInputHasNumberOfTermsEqualToClassifierProviderTest { hasNumberOfTermsEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -86,7 +86,7 @@ class RatioInputHasNumberOfTermsEqualToClassifierProviderTest { hasNumberOfTermsEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -105,7 +105,7 @@ class RatioInputHasNumberOfTermsEqualToClassifierProviderTest { hasNumberOfTermsEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProviderTest.kt index dbddb7e5483..81968647149 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -49,7 +49,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -66,7 +66,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // The ratio has value 3, but the value 2 was expected. @@ -84,7 +84,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A value was expected at index 2, but the ratio doesn't have that. @@ -99,7 +99,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -113,7 +113,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // The ratio has value 3 at index 1, but the value 2 was expected. @@ -128,7 +128,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -142,7 +142,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // The ratio has value 2 at index 2, but the value 3 was expected. @@ -157,7 +157,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A value was expected at index 3, but the ratio doesn't have that. @@ -172,7 +172,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A value was expected at index 0, but the ratio doesn't have that. @@ -187,7 +187,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A value was expected at index 4, but the ratio doesn't have that. @@ -202,7 +202,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -219,7 +219,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -236,7 +236,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -253,7 +253,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -268,7 +268,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = mapOf(), - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProviderTest.kt index 9b1e17c3c42..c93b44f8dc2 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -66,7 +66,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_2_4_6, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -80,7 +80,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -94,7 +94,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -108,7 +108,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -122,7 +122,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -136,7 +136,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -150,7 +150,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -169,7 +169,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProviderTest.kt index 8c931bc5009..11c16049edc 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProviderTest.kt @@ -13,7 +13,7 @@ import dagger.Provides import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createString import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableSetOfNormalizedString @@ -95,7 +95,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -109,7 +109,7 @@ class TextInputContainsRuleClassifierProviderTest { inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_NULL, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -122,7 +122,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -135,7 +135,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_IS_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -148,7 +148,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_NOT_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -161,7 +161,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -174,7 +174,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -187,7 +187,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -200,7 +200,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -213,7 +213,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -227,7 +227,7 @@ class TextInputContainsRuleClassifierProviderTest { inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_NULL, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -240,7 +240,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -253,7 +253,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -267,7 +267,7 @@ class TextInputContainsRuleClassifierProviderTest { inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -284,7 +284,7 @@ class TextInputContainsRuleClassifierProviderTest { inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -302,7 +302,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = createString("an answer among many"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -315,7 +315,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = createString("uma resposta entre muitas"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A Portuguese answer isn't reocgnized with this translation context. @@ -329,7 +329,9 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = createString("an answer among many"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // Even though the English string matches, the presence of the Portuguese context should trigger @@ -344,7 +346,9 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = createString("uma resposta entre muitas"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The translation context provides a bridge between Portuguese & English. @@ -358,7 +362,9 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = createString("de outros"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The Portuguese answer doesn't match. diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProviderTest.kt index 59dc509ca7f..6506df027fd 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProviderTest.kt @@ -13,7 +13,7 @@ import dagger.Provides import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createString import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableSetOfNormalizedString @@ -87,7 +87,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -101,7 +101,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -115,7 +115,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -129,7 +129,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_SINGLE_SPACES, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -142,7 +142,7 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_DIFFERENT_VALUE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -156,7 +156,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_SINGLE_SPACES, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -169,7 +169,7 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = STRING_VALUE_THIS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -182,7 +182,7 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = STRING_VALUE_A_TEST, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -195,7 +195,7 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = STRING_VALUE_NOT_A_TEST, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -209,7 +209,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -226,7 +226,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -244,7 +244,7 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = createString("an answer"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -257,7 +257,7 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = createString("uma resposta"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A Portuguese answer isn't reocgnized with this translation context. @@ -271,7 +271,9 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = createString("an answer"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // Even though the English string matches, the presence of the Portuguese context should trigger @@ -286,7 +288,9 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = createString("uma resposta"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The translation context provides a bridge between Portuguese & English. @@ -300,7 +304,9 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = createString("uma resposta diferente"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The Portuguese answer doesn't match. diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProviderTest.kt index 0642bc8879c..53617305bd3 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProviderTest.kt @@ -13,7 +13,7 @@ import dagger.Provides import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createString import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableSetOfNormalizedString @@ -89,7 +89,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -103,7 +103,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -117,7 +117,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -131,7 +131,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_DIFF_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -145,7 +145,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -159,7 +159,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -173,7 +173,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_DIFF_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -187,7 +187,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_DIFF_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -200,7 +200,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -213,7 +213,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_WITH_WHITESPACE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -226,7 +226,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_THIS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -239,7 +239,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -252,7 +252,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TESTING, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -266,7 +266,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -284,7 +284,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("an answer"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -297,7 +297,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("uma resposta"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A Portuguese answer isn't reocgnized with this translation context. @@ -311,7 +311,9 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("an answer"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // Even though the English string matches, the presence of the Portuguese context should trigger @@ -326,7 +328,9 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("uma resposta"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The translation context provides a bridge between Portuguese & English. @@ -340,7 +344,9 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("uma reposta"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // A single misspelled letter should still result in a match in the same way as English. @@ -354,7 +360,9 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("reposta"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // A missing word & a misspelled word should result in no match. @@ -368,7 +376,9 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("إجاب"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "إجابة") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "إجابة") + ) ) // A single misspelled letter should still result in a match in the same way as English. @@ -382,7 +392,9 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("إجا"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // Multiple missing letters should result in no match. diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProviderTest.kt index 1d0cd08ece0..1999f7ca984 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProviderTest.kt @@ -13,7 +13,7 @@ import dagger.Provides import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createString import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableSetOfNormalizedString @@ -93,7 +93,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -106,7 +106,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -119,7 +119,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -132,7 +132,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // The check should be case-insensitive. @@ -146,7 +146,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -159,7 +159,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // The check should be case-insensitive. @@ -173,7 +173,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -186,7 +186,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -199,7 +199,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -212,7 +212,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -225,7 +225,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_NULL, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -238,7 +238,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_NULL, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -251,7 +251,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_ANTIDERIVATIVE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -264,7 +264,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_PREFIX, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -277,7 +277,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_SOMETHING_ELSE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -291,7 +291,7 @@ class TextInputStartsWithRuleClassifierProviderTest { inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -308,7 +308,7 @@ class TextInputStartsWithRuleClassifierProviderTest { inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -326,7 +326,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = createString("an answer is my choice"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -339,7 +339,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = createString("uma resposta é minha escolha"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A Portuguese answer isn't reocgnized with this translation context. @@ -353,7 +353,9 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = createString("an answer is my choice"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // Even though the English string matches, the presence of the Portuguese context should trigger @@ -368,7 +370,9 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = createString("uma resposta é minha escolha"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The translation context provides a bridge between Portuguese & English. @@ -382,7 +386,9 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = createString("diferente"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The Portuguese answer doesn't match. From fef2d976ec89ad02c198c7d25b743ba762424812 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 17:47:07 -0800 Subject: [PATCH 116/289] Add classifiers for AlgebraicExpressionInput. --- ...putIsEquivalentToRuleClassifierProvider.kt | 69 +++++++++++++++++++ ...atchesExactlyWithRuleClassifierProvider.kt | 62 +++++++++++++++++ ...vialManipulationsRuleClassifierProvider.kt | 64 +++++++++++++++++ .../AlgebraicExpressionInputModule.kt | 36 ++++++++++ 4 files changed, 231 insertions(+) create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt new file mode 100644 index 00000000000..899a14682d6 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -0,0 +1,69 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.Polynomial +import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.GenericRuleClassifier +import org.oppia.android.domain.classify.rules.RuleClassifierProvider +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.toPolynomial +import javax.inject.Inject +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression + +class AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + classificationContext: ClassificationContext + ): Boolean { + val allowedVariables = classificationContext.extractAllowedVariables() + val answerExpression = parsePolynomial(answer, allowedVariables) ?: return false + val inputExpression = parsePolynomial(input, allowedVariables) ?: return false + return answerExpression == inputExpression + } + + private fun parsePolynomial(rawExpression: String, allowedVariables: List): Polynomial? { + return when (val expResult = parseAlgebraicExpression(rawExpression, allowedVariables)) { + is MathParsingResult.Success -> { + expResult.result.toPolynomial().also { + if (it == null) { + consoleLogger.w( + "AlgebraExpEquivalent", "Expression is not a supported polynomial: $rawExpression." + ) + } + } + } + is MathParsingResult.Failure -> { + consoleLogger.e( + "AlgebraExpEquivalent", + "Encountered expression that failed parsing. Expression: $rawExpression." + + " Failure: ${expResult.error}." + ) + null + } + } + } + + private companion object { + private fun ClassificationContext.extractAllowedVariables(): List { + return customizationArgs["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt new file mode 100644 index 00000000000..fb521383d4c --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -0,0 +1,62 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.MathExpression +import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.GenericRuleClassifier +import org.oppia.android.domain.classify.rules.RuleClassifierProvider +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import javax.inject.Inject +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression + +class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + classificationContext: ClassificationContext + ): Boolean { + val allowedVariables = classificationContext.extractAllowedVariables() + val answerExpression = parseExpression(answer, allowedVariables) ?: return false + val inputExpression = parseExpression(input, allowedVariables) ?: return false + return answerExpression == inputExpression + } + + private fun parseExpression( + rawExpression: String, allowedVariables: List + ): MathExpression? { + return when (val expResult = parseAlgebraicExpression(rawExpression, allowedVariables)) { + is MathParsingResult.Success -> expResult.result + is MathParsingResult.Failure -> { + consoleLogger.e( + "AlgebraExpMatchesExact", + "Encountered expression that failed parsing. Expression: $rawExpression." + + " Failure: ${expResult.error}." + ) + null + } + } + } + + private companion object { + private fun ClassificationContext.extractAllowedVariables(): List { + return customizationArgs["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt new file mode 100644 index 00000000000..b56fe353d39 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -0,0 +1,64 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.GenericRuleClassifier +import org.oppia.android.domain.classify.rules.RuleClassifierProvider +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.toComparableOperationList +import javax.inject.Inject +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression + +class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider +@Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + classificationContext: ClassificationContext + ): Boolean { + val allowedVariables = classificationContext.extractAllowedVariables() + val answerExpression = parseComparableOperationList(answer, allowedVariables) ?: return false + val inputExpression = parseComparableOperationList(input, allowedVariables) ?: return false + return answerExpression == inputExpression + } + + private fun parseComparableOperationList( + rawExpression: String, allowedVariables: List + ): ComparableOperationList? { + return when (val expResult = parseAlgebraicExpression(rawExpression, allowedVariables)) { + is MathParsingResult.Success -> expResult.result.toComparableOperationList() + is MathParsingResult.Failure -> { + consoleLogger.e( + "AlgebraExpTrivialManips", + "Encountered expression that failed parsing. Expression: $rawExpression." + + " Failure: ${expResult.error}." + ) + null + } + } + } + + private companion object { + private fun ClassificationContext.extractAllowedVariables(): List { + return customizationArgs["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt new file mode 100644 index 00000000000..091dc12e4e6 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt @@ -0,0 +1,36 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import dagger.multibindings.StringKey +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.FractionInputRules + +/** Module that binds rule classifiers corresponding to the algebraic expression input interaction. */ +@Module +class AlgebraicExpressionInputModule { + @Provides + @IntoMap + @StringKey("MatchesExactlyWith") + @FractionInputRules + internal fun provideAlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider( + classifierProvider: AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() + + @Provides + @IntoMap + @StringKey("MatchesUpToTrivialManipulations") + @FractionInputRules + internal fun provideAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( + classifierProvider: AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() + + @Provides + @IntoMap + @StringKey("IsEquivalentTo") + @FractionInputRules + internal fun provideAlgebraicExpressionInputIsEquivalentToRuleClassifier( + classifierProvider: AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() +} From 783396f6998eddde37ff61d8c3f3662bb44c5e22 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 17:49:16 -0800 Subject: [PATCH 117/289] Add missing annotation for new interaction. --- .../domain/classify/rules/RuleQualifiers.kt | 57 +++++++++++++++---- .../NumericExpressionInputModule.kt | 8 +-- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt index 18b9d9faed7..a8965af13ae 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt @@ -2,42 +2,79 @@ package org.oppia.android.domain.classify.rules import javax.inject.Qualifier -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the continue interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * continue interaction. + */ @Qualifier annotation class ContinueRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the fraction input interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * fraction input interaction. + */ @Qualifier annotation class FractionInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the item selection interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the item + * selection interaction. + */ @Qualifier annotation class ItemSelectionInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the multiple choice interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * multiple choice interaction. + */ @Qualifier annotation class MultipleChoiceInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the number with units interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the number + * with units interaction. + */ @Qualifier annotation class NumberWithUnitsRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the text input interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the text + * input interaction. + */ @Qualifier annotation class TextInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the numeric input interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * numeric input interaction. + */ @Qualifier annotation class NumericInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the drag drop sort input interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the drag + * drop sort input interaction. + */ @Qualifier annotation class DragDropSortInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the image click input interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the image + * click input interaction. + */ @Qualifier annotation class ImageClickInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the ratio input interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the ratio + * input interaction. + */ @Qualifier annotation class RatioExpressionInputRules + +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * numeric expression input interaction. + */ +@Qualifier +annotation class NumericExpressionInputRules diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt index cb010da48de..4a74256a777 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt @@ -5,7 +5,7 @@ import dagger.Provides import dagger.multibindings.IntoMap import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier -import org.oppia.android.domain.classify.rules.FractionInputRules +import org.oppia.android.domain.classify.rules.NumericExpressionInputRules /** Module that binds rule classifiers corresponding to the numeric expression input interaction. */ @Module @@ -13,7 +13,7 @@ class NumericExpressionInputModule { @Provides @IntoMap @StringKey("MatchesExactlyWith") - @FractionInputRules + @NumericExpressionInputRules internal fun provideNumericExpressionInputMatchesExactlyWithRuleClassifierProvider( classifierProvider: NumericExpressionInputMatchesExactlyWithRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @@ -21,7 +21,7 @@ class NumericExpressionInputModule { @Provides @IntoMap @StringKey("MatchesUpToTrivialManipulations") - @FractionInputRules + @NumericExpressionInputRules internal fun provideNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( classifierProvider: NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @@ -29,7 +29,7 @@ class NumericExpressionInputModule { @Provides @IntoMap @StringKey("IsEquivalentTo") - @FractionInputRules + @NumericExpressionInputRules internal fun provideNumericExpressionInputIsEquivalentToRuleClassifier( classifierProvider: NumericExpressionInputIsEquivalentToRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() From 69eaf25db8971d0bb8ae0539de09963714e8398e Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 17:50:36 -0800 Subject: [PATCH 118/289] Add missing annotation for new interaction. --- .../oppia/android/domain/classify/rules/RuleQualifiers.kt | 7 +++++++ .../AlgebraicExpressionInputModule.kt | 8 ++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt index a8965af13ae..9c38cbcef98 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt @@ -78,3 +78,10 @@ annotation class RatioExpressionInputRules */ @Qualifier annotation class NumericExpressionInputRules + +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * algebraic expression input interaction. + */ +@Qualifier +annotation class AlgebraicExpressionInputRules diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt index 091dc12e4e6..88019af6562 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt @@ -5,7 +5,7 @@ import dagger.Provides import dagger.multibindings.IntoMap import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier -import org.oppia.android.domain.classify.rules.FractionInputRules +import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules /** Module that binds rule classifiers corresponding to the algebraic expression input interaction. */ @Module @@ -13,7 +13,7 @@ class AlgebraicExpressionInputModule { @Provides @IntoMap @StringKey("MatchesExactlyWith") - @FractionInputRules + @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider( classifierProvider: AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @@ -21,7 +21,7 @@ class AlgebraicExpressionInputModule { @Provides @IntoMap @StringKey("MatchesUpToTrivialManipulations") - @FractionInputRules + @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( classifierProvider: AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @@ -29,7 +29,7 @@ class AlgebraicExpressionInputModule { @Provides @IntoMap @StringKey("IsEquivalentTo") - @FractionInputRules + @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputIsEquivalentToRuleClassifier( classifierProvider: AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() From 86256d948c60be6510022ee76af6983b704a7ddf Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 17:51:38 -0800 Subject: [PATCH 119/289] Lint fixes. --- ...cExpressionInputIsEquivalentToRuleClassifierProvider.kt | 2 +- ...ressionInputMatchesExactlyWithRuleClassifierProvider.kt | 5 +++-- ...atchesUpToTrivialManipulationsRuleClassifierProvider.kt | 5 +++-- .../AlgebraicExpressionInputModule.kt | 7 +++++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt index 899a14682d6..276e9de2868 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -8,9 +8,9 @@ import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import org.oppia.android.util.math.toPolynomial import javax.inject.Inject -import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression class AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index fb521383d4c..b23d434d2d6 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -8,8 +8,8 @@ import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult -import javax.inject.Inject import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression +import javax.inject.Inject class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -35,7 +35,8 @@ class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject c } private fun parseExpression( - rawExpression: String, allowedVariables: List + rawExpression: String, + allowedVariables: List ): MathExpression? { return when (val expResult = parseAlgebraicExpression(rawExpression, allowedVariables)) { is MathParsingResult.Success -> expResult.result diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index b56fe353d39..a7062556901 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -8,9 +8,9 @@ import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import org.oppia.android.util.math.toComparableOperationList import javax.inject.Inject -import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( @@ -37,7 +37,8 @@ class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvi } private fun parseComparableOperationList( - rawExpression: String, allowedVariables: List + rawExpression: String, + allowedVariables: List ): ComparableOperationList? { return when (val expResult = parseAlgebraicExpression(rawExpression, allowedVariables)) { is MathParsingResult.Success -> expResult.result.toComparableOperationList() diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt index 88019af6562..810f868808a 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt @@ -7,7 +7,9 @@ import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules -/** Module that binds rule classifiers corresponding to the algebraic expression input interaction. */ +/** + * Module that binds rule classifiers corresponding to the algebraic expression input interaction. + */ @Module class AlgebraicExpressionInputModule { @Provides @@ -23,7 +25,8 @@ class AlgebraicExpressionInputModule { @StringKey("MatchesUpToTrivialManipulations") @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( - classifierProvider: AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + classifierProvider: + AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @Provides From 07dcc94fad4cdb60cade7da79d7a4c78a3620b53 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 18:07:05 -0800 Subject: [PATCH 120/289] Add math equation input classifiers. --- .../domain/classify/rules/RuleQualifiers.kt | 7 ++ .../AlgebraicExpressionInputModule.kt | 9 ++- ...putIsEquivalentToRuleClassifierProvider.kt | 78 +++++++++++++++++++ ...atchesExactlyWithRuleClassifierProvider.kt | 63 +++++++++++++++ ...vialManipulationsRuleClassifierProvider.kt | 71 +++++++++++++++++ .../MathEquationInputModule.kt | 37 +++++++++ 6 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModule.kt diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt index 9c38cbcef98..ec08463b944 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt @@ -85,3 +85,10 @@ annotation class NumericExpressionInputRules */ @Qualifier annotation class AlgebraicExpressionInputRules + +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * math equation input interaction. + */ +@Qualifier +annotation class MathEquationInputRules diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt index 810f868808a..bdff7180826 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt @@ -6,6 +6,9 @@ import dagger.multibindings.IntoMap import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputIsEquivalentToRuleClassifierProvider +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputMatchesExactlyWithRuleClassifierProvider +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider /** * Module that binds rule classifiers corresponding to the algebraic expression input interaction. @@ -17,7 +20,7 @@ class AlgebraicExpressionInputModule { @StringKey("MatchesExactlyWith") @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider( - classifierProvider: AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider + classifierProvider: MathEquationInputMatchesExactlyWithRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @Provides @@ -26,7 +29,7 @@ class AlgebraicExpressionInputModule { @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( classifierProvider: - AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @Provides @@ -34,6 +37,6 @@ class AlgebraicExpressionInputModule { @StringKey("IsEquivalentTo") @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputIsEquivalentToRuleClassifier( - classifierProvider: AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider + classifierProvider: MathEquationInputIsEquivalentToRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt new file mode 100644 index 00000000000..07de228a104 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt @@ -0,0 +1,78 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.Polynomial +import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.GenericRuleClassifier +import org.oppia.android.domain.classify.rules.RuleClassifierProvider +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation +import org.oppia.android.util.math.toPolynomial +import javax.inject.Inject + +class MathEquationInputIsEquivalentToRuleClassifierProvider @Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + classificationContext: ClassificationContext + ): Boolean { + val allowedVariables = classificationContext.extractAllowedVariables() + val (answerLhs, answerRhs) = parsePolynomials(answer, allowedVariables) ?: return false + val (inputLhs, inputRhs) = parsePolynomials(input, allowedVariables) ?: return false + + // Sides may cross-match (i.e. it's fine to reorder around the '='). + return (answerLhs == inputLhs && answerRhs == inputRhs) || + (answerLhs == inputRhs && answerRhs == inputLhs) + } + + private fun parsePolynomials( + rawEquation: String, + allowedVariables: List + ): Pair? { + return when (val eqResult = parseAlgebraicEquation(rawEquation, allowedVariables)) { + is MathParsingResult.Success -> { + val lhsExp = eqResult.result.leftSide.toPolynomial() + val rhsExp = eqResult.result.rightSide.toPolynomial() + if (lhsExp != null && rhsExp != null) { + lhsExp to rhsExp + } else { + consoleLogger.w( + "AlgebraEqEquivalent", "Equation is not a supported polynomial: $rawEquation." + ) + null + } + } + is MathParsingResult.Failure -> { + consoleLogger.e( + "AlgebraEqEquivalent", + "Encountered equation that failed parsing. Equation: $rawEquation." + + " Failure: ${eqResult.error}." + ) + null + } + } + } + + private companion object { + private fun ClassificationContext.extractAllowedVariables(): List { + return customizationArgs["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt new file mode 100644 index 00000000000..3bb7e3d875c --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt @@ -0,0 +1,63 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.MathEquation +import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.GenericRuleClassifier +import org.oppia.android.domain.classify.rules.RuleClassifierProvider +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation +import javax.inject.Inject + +class MathEquationInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + classificationContext: ClassificationContext + ): Boolean { + val allowedVariables = classificationContext.extractAllowedVariables() + val answerEquation = parseEquation(answer, allowedVariables) ?: return false + val inputEquation = parseEquation(input, allowedVariables) ?: return false + return answerEquation == inputEquation + } + + private fun parseEquation( + rawEquation: String, + allowedVariables: List + ): MathEquation? { + return when (val eqResult = parseAlgebraicEquation(rawEquation, allowedVariables)) { + is MathParsingResult.Success -> eqResult.result + is MathParsingResult.Failure -> { + consoleLogger.e( + "AlgebraEqMatchesExact", + "Encountered equation that failed parsing. Equation: $rawEquation." + + " Failure: ${eqResult.error}." + ) + null + } + } + } + + private companion object { + private fun ClassificationContext.extractAllowedVariables(): List { + return customizationArgs["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt new file mode 100644 index 00000000000..64b5e6215f6 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -0,0 +1,71 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.GenericRuleClassifier +import org.oppia.android.domain.classify.rules.RuleClassifierProvider +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation +import org.oppia.android.util.math.toComparableOperationList +import javax.inject.Inject + +class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider +@Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + classificationContext: ClassificationContext + ): Boolean { + val allowedVariables = classificationContext.extractAllowedVariables() + val (answerLhs, answerRhs) = parseComparableLists(answer, allowedVariables) ?: return false + val (inputLhs, inputRhs) = parseComparableLists(input, allowedVariables) ?: return false + + // Sides must match (reordering around the '=' is not allowed by this classifier). + return answerLhs == inputLhs && answerRhs == inputRhs + } + + private fun parseComparableLists( + rawEquation: String, + allowedVariables: List + ): Pair? { + return when (val eqResult = parseAlgebraicEquation(rawEquation, allowedVariables)) { + is MathParsingResult.Success -> { + val lhsExp = eqResult.result.leftSide + val rhsExp = eqResult.result.rightSide + lhsExp.toComparableOperationList() to rhsExp.toComparableOperationList() + } + is MathParsingResult.Failure -> { + consoleLogger.e( + "AlgebraEqTrivialManips", + "Encountered equation that failed parsing. Equation: $rawEquation." + + " Failure: ${eqResult.error}." + ) + null + } + } + } + + private companion object { + private fun ClassificationContext.extractAllowedVariables(): List { + return customizationArgs["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModule.kt new file mode 100644 index 00000000000..8e8bc03f75d --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModule.kt @@ -0,0 +1,37 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import dagger.multibindings.StringKey +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.MathEquationInputRules + +/** Module that binds rule classifiers corresponding to the math equation input interaction. */ +@Module +class MathEquationInputModule { + @Provides + @IntoMap + @StringKey("MatchesExactlyWith") + @MathEquationInputRules + internal fun provideMathEquationInputMatchesExactlyWithRuleClassifier( + classifierProvider: MathEquationInputMatchesExactlyWithRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() + + @Provides + @IntoMap + @StringKey("MatchesUpToTrivialManipulations") + @MathEquationInputRules + internal fun provideMathEquationInputMatchesUpToTrivialManipulationsRuleClassifier( + classifierProvider: + MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() + + @Provides + @IntoMap + @StringKey("IsEquivalentTo") + @MathEquationInputRules + internal fun provideMathEquationInputIsEquivalentToRuleClassifier( + classifierProvider: MathEquationInputIsEquivalentToRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() +} From 42684b9e2250a7a613835cd9cd75e0b89c636a85 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 18:07:36 -0800 Subject: [PATCH 121/289] Fix provider name. --- .../numericexpressioninput/NumericExpressionInputModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt index 4a74256a777..ca42ea19de0 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt @@ -14,7 +14,7 @@ class NumericExpressionInputModule { @IntoMap @StringKey("MatchesExactlyWith") @NumericExpressionInputRules - internal fun provideNumericExpressionInputMatchesExactlyWithRuleClassifierProvider( + internal fun provideNumericExpressionInputMatchesExactlyWithRuleClassifier( classifierProvider: NumericExpressionInputMatchesExactlyWithRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() From aff5bc69fb64c85c466379b3cfe8580ee7190877 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 18:08:11 -0800 Subject: [PATCH 122/289] Fix provider name. --- .../algebraicexpressioninput/AlgebraicExpressionInputModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt index 810f868808a..9923355076f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt @@ -16,7 +16,7 @@ class AlgebraicExpressionInputModule { @IntoMap @StringKey("MatchesExactlyWith") @AlgebraicExpressionInputRules - internal fun provideAlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider( + internal fun provideAlgebraicExpressionInputMatchesExactlyWithRuleClassifier( classifierProvider: AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() From ebd2073b864a1b0b00eeca5626f57d14d3233f9a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 18:10:39 -0800 Subject: [PATCH 123/289] Fix module regression from earlier commit. --- .../AlgebraicExpressionInputModule.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt index 1fe035f285d..9923355076f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt @@ -6,9 +6,6 @@ import dagger.multibindings.IntoMap import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules -import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputIsEquivalentToRuleClassifierProvider -import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputMatchesExactlyWithRuleClassifierProvider -import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider /** * Module that binds rule classifiers corresponding to the algebraic expression input interaction. @@ -29,7 +26,7 @@ class AlgebraicExpressionInputModule { @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( classifierProvider: - MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider + AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @Provides @@ -37,6 +34,6 @@ class AlgebraicExpressionInputModule { @StringKey("IsEquivalentTo") @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputIsEquivalentToRuleClassifier( - classifierProvider: MathEquationInputIsEquivalentToRuleClassifierProvider + classifierProvider: AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() } From 46b765567d194cfb0e2c7f616f0ae9bf238ac944 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 22:23:14 -0800 Subject: [PATCH 124/289] Enable new math classifiers. Also, add the classifiers' new modules to all affected tests, plus both the prod and instrumentation application components. --- .../app/application/ApplicationComponent.kt | 5 +++ .../AdministratorControlsActivityTest.kt | 7 ++++- .../AppVersionActivityTest.kt | 7 ++++- .../CompletedStoryListActivityTest.kt | 7 ++++- .../LessonThumbnailImageViewTest.kt | 7 ++++- .../ImageViewBindingAdaptersTest.kt | 7 ++++- .../databinding/MarginBindingAdaptersTest.kt | 7 ++++- ...StateAssemblerMarginBindingAdaptersTest.kt | 7 ++++- ...tateAssemblerPaddingBindingAdaptersTest.kt | 7 ++++- .../databinding/ViewBindingAdaptersTest.kt | 7 ++++- .../DeveloperOptionsActivityTest.kt | 7 ++++- .../DeveloperOptionsFragmentTest.kt | 7 ++++- .../MarkChaptersCompletedActivityTest.kt | 7 ++++- .../MarkChaptersCompletedFragmentTest.kt | 7 ++++- .../MarkStoriesCompletedActivityTest.kt | 7 ++++- .../MarkStoriesCompletedFragmentTest.kt | 7 ++++- .../MarkTopicsCompletedActivityTest.kt | 7 ++++- .../MarkTopicsCompletedFragmentTest.kt | 7 ++++- .../devoptions/ViewEventLogsActivityTest.kt | 7 ++++- .../devoptions/ViewEventLogsFragmentTest.kt | 7 ++++- .../ForceNetworkTypeActivityTest.kt | 7 ++++- .../ForceNetworkTypeFragmentTest.kt | 7 ++++- .../android/app/faq/FAQListFragmentTest.kt | 7 ++++- .../android/app/faq/FAQSingleActivityTest.kt | 7 ++++- .../android/app/faq/FaqListActivityTest.kt | 7 ++++- .../android/app/help/HelpActivityTest.kt | 7 ++++- .../android/app/help/HelpFragmentTest.kt | 7 ++++- .../android/app/home/HomeActivityTest.kt | 7 ++++- .../app/home/RecentlyPlayedFragmentTest.kt | 7 ++++- .../app/home/TopicSummaryViewModelTest.kt | 7 ++++- .../android/app/home/WelcomeViewModelTest.kt | 7 ++++- .../PromotedStoryListViewModelTest.kt | 7 ++++- .../PromotedStoryViewModelTest.kt | 7 ++++- .../mydownloads/MyDownloadsFragmentTest.kt | 7 ++++- .../app/onboarding/OnboardingActivityTest.kt | 7 ++++- .../app/onboarding/OnboardingFragmentTest.kt | 7 ++++- .../OngoingTopicListActivityTest.kt | 7 ++++- .../app/options/AppLanguageActivityTest.kt | 7 ++++- .../app/options/AppLanguageFragmentTest.kt | 7 ++++- .../app/options/AudioLanguageActivityTest.kt | 7 ++++- .../app/options/AudioLanguageFragmentTest.kt | 7 ++++- .../app/options/OptionsActivityTest.kt | 7 ++++- .../app/options/OptionsFragmentTest.kt | 7 ++++- .../options/ReadingTextSizeActivityTest.kt | 7 ++++- .../options/ReadingTextSizeFragmentTest.kt | 7 ++++- .../app/parser/CustomBulletSpanTest.kt | 7 ++++- .../android/app/parser/HtmlParserTest.kt | 7 ++++- .../app/player/audio/AudioFragmentTest.kt | 7 ++++- .../exploration/ExplorationActivityTest.kt | 7 ++++- .../app/player/state/StateFragmentTest.kt | 7 ++++- .../app/profile/AddProfileActivityTest.kt | 7 ++++- .../app/profile/AdminAuthActivityTest.kt | 7 ++++- .../app/profile/AdminPinActivityTest.kt | 7 ++++- .../app/profile/PinPasswordActivityTest.kt | 7 ++++- .../app/profile/ProfileChooserFragmentTest.kt | 7 ++++- .../ProfilePictureActivityTest.kt | 7 ++++- .../ProfileProgressActivityTest.kt | 7 ++++- .../ProfileProgressFragmentTest.kt | 7 ++++- .../app/recyclerview/BindableAdapterTest.kt | 7 ++++- .../resumelesson/ResumeLessonActivityTest.kt | 7 ++++- .../resumelesson/ResumeLessonFragmentTest.kt | 7 ++++- .../profile/ProfileEditActivityTest.kt | 7 ++++- .../profile/ProfileListActivityTest.kt | 7 ++++- .../profile/ProfileListFragmentTest.kt | 7 ++++- .../profile/ProfileRenameActivityTest.kt | 7 ++++- .../profile/ProfileRenameFragmentTest.kt | 7 ++++- .../profile/ProfileResetPinActivityTest.kt | 7 ++++- .../android/app/splash/SplashActivityTest.kt | 7 ++++- .../android/app/story/StoryActivityTest.kt | 7 ++++- .../android/app/story/StoryFragmentTest.kt | 7 ++++- .../app/testing/DragDropTestActivityTest.kt | 7 ++++- ...ImageRegionSelectionInteractionViewTest.kt | 7 ++++- .../InputInteractionViewTestActivityTest.kt | 7 ++++- .../NavigationDrawerActivityDebugTest.kt | 7 ++++- .../NavigationDrawerActivityProdTest.kt | 6 +++- ...tFontScaleConfigurationUtilActivityTest.kt | 7 ++++- .../testing/TopicTestActivityForStoryTest.kt | 7 ++++- .../app/thirdparty/LicenseListActivityTest.kt | 7 ++++- .../app/thirdparty/LicenseListFragmentTest.kt | 7 ++++- .../LicenseTextViewerActivityTest.kt | 7 ++++- .../LicenseTextViewerFragmentTest.kt | 7 ++++- .../ThirdPartyDependencyListActivityTest.kt | 7 ++++- .../ThirdPartyDependencyListFragmentTest.kt | 7 ++++- .../android/app/topic/TopicActivityTest.kt | 7 ++++- .../android/app/topic/TopicFragmentTest.kt | 7 ++++- .../conceptcard/ConceptCardFragmentTest.kt | 7 ++++- .../app/topic/info/TopicInfoFragmentTest.kt | 7 ++++- .../topic/lessons/TopicLessonsFragmentTest.kt | 7 ++++- .../practice/TopicPracticeFragmentTest.kt | 7 ++++- .../QuestionPlayerActivityTest.kt | 7 ++++- .../revision/TopicRevisionFragmentTest.kt | 7 ++++- .../revisioncard/RevisionCardActivityTest.kt | 7 ++++- .../revisioncard/RevisionCardFragmentTest.kt | 7 ++++- .../app/utility/RatioExtensionsTest.kt | 7 ++++- .../walkthrough/WalkthroughActivityTest.kt | 7 ++++- .../WalkthroughFinalFragmentTest.kt | 7 ++++- .../WalkthroughTopicListFragmentTest.kt | 7 ++++- .../WalkthroughWelcomeFragmentTest.kt | 7 ++++- .../activity/ActivityIntentFactoriesTest.kt | 7 ++++- .../android/app/home/HomeActivityLocalTest.kt | 6 +++- .../app/parser/StringToFractionParserTest.kt | 7 ++++- .../app/parser/StringToRatioParserTest.kt | 7 ++++- .../ExplorationActivityLocalTest.kt | 6 +++- .../player/state/StateFragmentLocalTest.kt | 7 ++++- .../ProfileChooserFragmentLocalTest.kt | 7 ++++- .../app/story/StoryActivityLocalTest.kt | 7 ++++- .../app/testing/CompletedStoryListSpanTest.kt | 6 +++- .../oppia/android/app/testing/HomeSpanTest.kt | 6 +++- .../app/testing/OngoingTopicListSpanTest.kt | 6 +++- .../PlatformParameterIntegrationTest.kt | 7 ++++- .../app/testing/ProfileChooserSpanTest.kt | 6 +++- .../app/testing/ProfileProgressSpanCount.kt | 6 +++- .../app/testing/RecentlyPlayedSpanTest.kt | 6 +++- .../app/testing/TopicRevisionSpanTest.kt | 6 +++- .../app/testing/activity/TestActivityTest.kt | 7 ++++- .../AdministratorControlsFragmentTest.kt | 7 ++++- .../testing/options/OptionsFragmentTest.kt | 7 ++++- .../player/split/PlayerSplitScreenTesting.kt | 6 +++- .../state/StateFragmentAccessibilityTest.kt | 6 +++- .../topic/info/TopicInfoFragmentLocalTest.kt | 7 ++++- .../lessons/TopicLessonsFragmentLocalTest.kt | 7 ++++- .../QuestionPlayerActivityLocalTest.kt | 7 ++++- .../RevisionCardActivityLocalTest.kt | 7 ++++- .../AppLanguageResourceHandlerTest.kt | 7 ++++- .../AppLanguageWatcherMixinTest.kt | 7 ++++- .../app/utility/datetime/DateTimeUtilTest.kt | 7 ++++- .../domain/classify/InteractionsModule.kt | 31 +++++++++++++++++++ .../AnswerClassificationControllerTest.kt | 7 ++++- .../ExplorationDataControllerTest.kt | 6 +++- .../ExplorationProgressControllerTest.kt | 6 +++- ...uestionAssessmentProgressControllerTest.kt | 6 +++- .../QuestionTrainingControllerTest.kt | 7 ++++- .../application/TestApplicationComponent.kt | 5 +++ ...alizeDefaultLocaleRuleCustomContextTest.kt | 7 ++++- ...InitializeDefaultLocaleRuleOmissionTest.kt | 7 ++++- .../junit/InitializeDefaultLocaleRuleTest.kt | 7 ++++- 136 files changed, 824 insertions(+), 133 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt b/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt index dbb6b7dd5d8..0f708fef5f8 100644 --- a/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt +++ b/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt @@ -14,13 +14,16 @@ import org.oppia.android.app.translation.ActivityRecreatorProdModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -93,6 +96,8 @@ import javax.inject.Singleton DeveloperOptionsModule::class, PlatformParameterSyncUpWorkerModule::class, NetworkConnectionUtilDebugModule::class, NetworkConfigProdModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorProdModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class, // TODO(#59): Remove this module once we completely migrate to Bazel from Gradle as we can then // directly exclude debug files from the build and thus won't be requiring this module. NetworkConnectionDebugUtilModule::class diff --git a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityTest.kt index d88d63e1650..159cce60276 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityTest.kt @@ -68,13 +68,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -719,7 +722,9 @@ class AdministratorControlsActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AppVersionActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AppVersionActivityTest.kt index 801af1bccff..6922b83fb3d 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AppVersionActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AppVersionActivityTest.kt @@ -48,13 +48,16 @@ import org.oppia.android.app.utility.getVersionName import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -279,7 +282,9 @@ class AppVersionActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt index 3ac470b78ad..d5117f9b248 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt @@ -49,13 +49,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -502,7 +505,9 @@ class CompletedStoryListActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/customview/LessonThumbnailImageViewTest.kt b/app/src/sharedTest/java/org/oppia/android/app/customview/LessonThumbnailImageViewTest.kt index 8b577dbc71d..01b3c5a1ee2 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/customview/LessonThumbnailImageViewTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/customview/LessonThumbnailImageViewTest.kt @@ -32,13 +32,16 @@ import org.oppia.android.app.utility.EspressoTestsMatchers.withDrawable import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -160,7 +163,9 @@ class LessonThumbnailImageViewTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/ImageViewBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/ImageViewBindingAdaptersTest.kt index d342a5e559d..b15930dedaf 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/ImageViewBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/ImageViewBindingAdaptersTest.kt @@ -42,13 +42,16 @@ import org.oppia.android.app.utility.EspressoTestsMatchers.withDrawable import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -209,7 +212,9 @@ class ImageViewBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) /** Create a TestApplicationComponent. */ diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/MarginBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/MarginBindingAdaptersTest.kt index c079fd4dda3..bb2a7fe3506 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/MarginBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/MarginBindingAdaptersTest.kt @@ -43,13 +43,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -297,7 +300,9 @@ class MarginBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) /** Create a TestApplicationComponent. */ diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerMarginBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerMarginBindingAdaptersTest.kt index 270c740a1d2..080a013ee66 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerMarginBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerMarginBindingAdaptersTest.kt @@ -45,13 +45,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -485,7 +488,9 @@ class StateAssemblerMarginBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) /** Create a TestApplicationComponent. */ diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerPaddingBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerPaddingBindingAdaptersTest.kt index ddcd62043c1..2d1f8204539 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerPaddingBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerPaddingBindingAdaptersTest.kt @@ -43,13 +43,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -483,7 +486,9 @@ class StateAssemblerPaddingBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt index dcfb063a37d..e01404ce617 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt @@ -40,13 +40,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -217,7 +220,9 @@ class ViewBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) /** Create a TestApplicationComponent. */ diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityTest.kt index fd2c18b6502..ddb40edd5a5 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityTest.kt @@ -52,13 +52,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -285,7 +288,9 @@ class DeveloperOptionsActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentTest.kt index 379f01c84f8..1443f901790 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentTest.kt @@ -52,13 +52,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -613,7 +616,9 @@ class DeveloperOptionsFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedActivityTest.kt index d5e639ba1d8..ee5330a99a6 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedActivityTest.kt @@ -35,13 +35,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -175,7 +178,9 @@ class MarkChaptersCompletedActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedFragmentTest.kt index 7505976ed81..2e966c81b4a 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedFragmentTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -879,7 +882,9 @@ class MarkChaptersCompletedFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedActivityTest.kt index 57b31d34318..fed5ad8a6ba 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedActivityTest.kt @@ -35,13 +35,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -175,7 +178,9 @@ class MarkStoriesCompletedActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedFragmentTest.kt index c29d95f046c..2385222bfb9 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedFragmentTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -580,7 +583,9 @@ class MarkStoriesCompletedFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedActivityTest.kt index 9e7bd458c57..4ab74ccc9ef 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedActivityTest.kt @@ -35,13 +35,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -175,7 +178,9 @@ class MarkTopicsCompletedActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedFragmentTest.kt index 26c9d35446f..1ba8cf8aa45 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedFragmentTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -550,7 +553,9 @@ class MarkTopicsCompletedFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsActivityTest.kt index 50a7fb1341e..52876fb584b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsActivityTest.kt @@ -36,13 +36,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -163,7 +166,9 @@ class ViewEventLogsActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsFragmentTest.kt index cf5fbb8c656..39d046d1bd8 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsFragmentTest.kt @@ -42,13 +42,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -619,7 +622,9 @@ class ViewEventLogsFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivityTest.kt index 852966ba144..91f7b1aab13 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivityTest.kt @@ -36,13 +36,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -167,7 +170,9 @@ class ForceNetworkTypeActivityTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) /** [ApplicationComponent] for [ForceNetworkTypeActivityTest]. */ diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentTest.kt index 3d42ca09eb8..09d87f436f1 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentTest.kt @@ -41,13 +41,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -383,7 +386,9 @@ class ForceNetworkTypeFragmentTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) /** [ApplicationComponent] for [ForceNetworkTypeFragmentTest]. */ diff --git a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt index df66cb56cd9..0175df9bc05 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt @@ -47,13 +47,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -235,7 +238,9 @@ class FAQListFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQSingleActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQSingleActivityTest.kt index e7dfe213e43..c6b1bdb48e6 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQSingleActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQSingleActivityTest.kt @@ -41,13 +41,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -207,7 +210,9 @@ class FAQSingleActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/faq/FaqListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/faq/FaqListActivityTest.kt index 28f0cb11667..d217168b930 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/faq/FaqListActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/faq/FaqListActivityTest.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -140,7 +143,9 @@ class FaqListActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/help/HelpActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/help/HelpActivityTest.kt index 69bad4b72cc..48375856775 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/help/HelpActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/help/HelpActivityTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -142,7 +145,9 @@ class HelpActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, NetworkModule::class, ExplorationStorageModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/help/HelpFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/help/HelpFragmentTest.kt index 2edef7165b0..7d45ce45b5c 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/help/HelpFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/help/HelpFragmentTest.kt @@ -54,13 +54,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1180,7 +1183,9 @@ class HelpFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt index d60ec860aec..388b7d6ae21 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt @@ -76,13 +76,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1745,7 +1748,9 @@ class HomeActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt index 0a525d1c9a9..ed091721a70 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt @@ -63,13 +63,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1494,7 +1497,9 @@ class RecentlyPlayedFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/TopicSummaryViewModelTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/TopicSummaryViewModelTest.kt index 958bf9b1f15..47af9a09fb0 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/TopicSummaryViewModelTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/TopicSummaryViewModelTest.kt @@ -31,13 +31,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -361,7 +364,9 @@ class TopicSummaryViewModelTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/WelcomeViewModelTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/WelcomeViewModelTest.kt index 65a83551f3e..4cd7decfaf1 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/WelcomeViewModelTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/WelcomeViewModelTest.kt @@ -31,13 +31,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -345,7 +348,9 @@ class WelcomeViewModelTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryListViewModelTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryListViewModelTest.kt index 13894d9a2d4..75ec22943e1 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryListViewModelTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryListViewModelTest.kt @@ -32,13 +32,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -357,7 +360,9 @@ class PromotedStoryListViewModelTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModelTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModelTest.kt index 1313cc9c660..3095b208e65 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModelTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModelTest.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -367,7 +370,9 @@ class PromotedStoryViewModelTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsFragmentTest.kt index 662eeef51a6..4e218635d4d 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsFragmentTest.kt @@ -39,13 +39,16 @@ import org.oppia.android.app.utility.EspressoTestsMatchers.matchCurrentTabTitle import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -221,7 +224,9 @@ class MyDownloadsFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingActivityTest.kt index af6b7269724..91b9c1164e5 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingActivityTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -139,7 +142,9 @@ class OnboardingActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt index 87389344071..3f6b2105c76 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt @@ -56,13 +56,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -682,7 +685,9 @@ class OnboardingFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt index 171f1b0e8af..396c3c2c8ae 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt @@ -48,13 +48,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -446,7 +449,9 @@ class OngoingTopicListActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageActivityTest.kt index 03da5a775d5..8b5e9873e87 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageActivityTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -149,7 +152,9 @@ class AppLanguageActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageFragmentTest.kt index 254e35c9113..df2e7442cae 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageFragmentTest.kt @@ -38,13 +38,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -244,7 +247,9 @@ class AppLanguageFragmentTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt index 7ecf36b535d..e91975c33d6 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -149,7 +152,9 @@ class AudioLanguageActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt index 66de1647b23..856ee69f349 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt @@ -37,13 +37,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -237,7 +240,9 @@ class AudioLanguageFragmentTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsActivityTest.kt index 5efa873b07c..09492b1bad0 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsActivityTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -141,7 +144,9 @@ class OptionsActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt index d6339898f06..246a0929c01 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt @@ -51,13 +51,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -674,7 +677,9 @@ class OptionsFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeActivityTest.kt index 5cd7267de8a..37f49d2f52c 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeActivityTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -149,7 +152,9 @@ class ReadingTextSizeActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt index 566c462e0db..08d958b81ba 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -303,7 +306,9 @@ class ReadingTextSizeFragmentTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/parser/CustomBulletSpanTest.kt b/app/src/sharedTest/java/org/oppia/android/app/parser/CustomBulletSpanTest.kt index ed7afe12eb6..159218cf20c 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/parser/CustomBulletSpanTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/parser/CustomBulletSpanTest.kt @@ -31,13 +31,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -241,7 +244,9 @@ class CustomBulletSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt b/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt index 2c95c3add3b..6362346e0fa 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt @@ -57,13 +57,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -643,7 +646,9 @@ class HtmlParserTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/audio/AudioFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/audio/AudioFragmentTest.kt index 50fbd545bdb..5f803b1f60a 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/audio/AudioFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/audio/AudioFragmentTest.kt @@ -53,13 +53,16 @@ import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.audio.AudioPlayerController import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -470,7 +473,9 @@ class AudioFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt index e79c1a441ce..16868b4f728 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt @@ -84,13 +84,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -2037,7 +2040,9 @@ class ExplorationActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, TestExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt index 6c0917f22b4..2fd6c8b24fd 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt @@ -103,13 +103,16 @@ import org.oppia.android.app.utility.clickPoint import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -2874,7 +2877,9 @@ class StateFragmentTest { ExplorationStorageModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, NetworkModule::class, NetworkConfigProdModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/AddProfileActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/AddProfileActivityTest.kt index 9ddc39c067d..021448bad2b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/AddProfileActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/AddProfileActivityTest.kt @@ -63,13 +63,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1711,7 +1714,9 @@ class AddProfileActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/AdminAuthActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/AdminAuthActivityTest.kt index f8638be04a1..a666d92be7b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/AdminAuthActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/AdminAuthActivityTest.kt @@ -49,13 +49,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -660,7 +663,9 @@ class AdminAuthActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/AdminPinActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/AdminPinActivityTest.kt index b028e931d58..1aa691c4a82 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/AdminPinActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/AdminPinActivityTest.kt @@ -58,13 +58,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1078,7 +1081,9 @@ class AdminPinActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt index 1b2943a2d3f..4578cd593b3 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt @@ -52,13 +52,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1151,7 +1154,9 @@ class PinPasswordActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt index 7138aac599c..aea8c488ca5 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt @@ -51,13 +51,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -518,7 +521,9 @@ class ProfileChooserFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfilePictureActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfilePictureActivityTest.kt index feea5e9b922..701b630d964 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfilePictureActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfilePictureActivityTest.kt @@ -37,13 +37,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -192,7 +195,9 @@ class ProfilePictureActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressActivityTest.kt index 8284f76cf64..4f2f42f5912 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressActivityTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -143,7 +146,9 @@ class ProfileProgressActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt index d7c34f86734..fe7ff24d608 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt @@ -69,13 +69,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -844,7 +847,9 @@ class ProfileProgressFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt b/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt index 8ca7d3dd9ba..e12136e90cd 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt @@ -68,13 +68,16 @@ import org.oppia.android.databinding.TestTextViewForIntWithDataBindingBinding import org.oppia.android.databinding.TestTextViewForLiveDataWithDataBindingBinding import org.oppia.android.databinding.TestTextViewForStringWithDataBindingBinding import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -756,7 +759,9 @@ class BindableAdapterTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonActivityTest.kt index 70e1c86d40d..17bbe1ad2b9 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonActivityTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -213,7 +216,9 @@ class ResumeLessonActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt index 93fa9266efc..ef1954c639e 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt @@ -42,13 +42,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -275,7 +278,9 @@ class ResumeLessonFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt index 3c11f43e592..7f351099569 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt @@ -49,13 +49,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -565,7 +568,9 @@ class ProfileEditActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListActivityTest.kt index 98b36aa1f3a..e4a8d7d8ba6 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListActivityTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -139,7 +142,9 @@ class ProfileListActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt index 841f849c063..5e6478efe3e 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -377,7 +380,9 @@ class ProfileListFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameActivityTest.kt index 16c2eb4085a..794efaf80ee 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameActivityTest.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -162,7 +165,9 @@ class ProfileRenameActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt index ca237fbd195..a9ecb8e5861 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt @@ -46,13 +46,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -451,7 +454,9 @@ class ProfileRenameFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinActivityTest.kt index f36a2a3a2a2..b3e697a860e 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinActivityTest.kt @@ -49,13 +49,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1040,7 +1043,9 @@ class ProfileResetPinActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt index 4dfa5800e92..d1c617371da 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt @@ -50,13 +50,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -481,7 +484,9 @@ class SplashActivityTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt index b0e77c6d24f..e8a9327bc5d 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt @@ -43,13 +43,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -223,7 +226,9 @@ class StoryActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt index e056e5d7dab..d1c7ccef311 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt @@ -77,13 +77,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -887,7 +890,9 @@ class StoryFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt index fe7173623e8..bcb8f2a1cf8 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt @@ -40,13 +40,16 @@ import org.oppia.android.app.utility.RecyclerViewCoordinatesProvider import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -212,7 +215,9 @@ class DragDropTestActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt index 3f12a269df1..9e3bfe67c80 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt @@ -51,13 +51,16 @@ import org.oppia.android.app.utility.clickPoint import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -379,7 +382,9 @@ class ImageRegionSelectionInteractionViewTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt index d9100849877..9b7437a6d02 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt @@ -45,13 +45,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1028,7 +1031,9 @@ class InputInteractionViewTestActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt index 9d23dd7c2d5..95365580884 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt @@ -66,13 +66,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -431,7 +434,9 @@ class NavigationDrawerActivityDebugTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt index f98a53fd6bc..32761957182 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt @@ -74,13 +74,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -980,7 +983,8 @@ class NavigationDrawerActivityProdTest { DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityTest.kt index 5ad3aa73abc..4536b24af0f 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityTest.kt @@ -35,13 +35,16 @@ import org.oppia.android.app.utility.FontSizeMatcher.Companion.withFontSize import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -190,7 +193,9 @@ class TestFontScaleConfigurationUtilActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/TopicTestActivityForStoryTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/TopicTestActivityForStoryTest.kt index 7e808fe423f..c706b9d6bae 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/TopicTestActivityForStoryTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/TopicTestActivityForStoryTest.kt @@ -41,13 +41,16 @@ import org.oppia.android.app.utility.EspressoTestsMatchers.matchCurrentTabTitle import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -187,7 +190,9 @@ class TopicTestActivityForStoryTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListActivityTest.kt index 64b472286ea..9c108b1aed1 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListActivityTest.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -151,7 +154,9 @@ class LicenseListActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListFragmentTest.kt index ef451afb505..ebdec396258 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListFragmentTest.kt @@ -46,13 +46,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -365,7 +368,9 @@ class LicenseListFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerActivityTest.kt index 9b55e0f23df..2f296d0a7dc 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerActivityTest.kt @@ -31,13 +31,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -160,7 +163,9 @@ class LicenseTextViewerActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt index c4f6c6789b5..4a9ab14597c 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt @@ -36,13 +36,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -343,7 +346,9 @@ class LicenseTextViewerFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListActivityTest.kt index 375e562c6b8..9de615de34c 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListActivityTest.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -148,7 +151,9 @@ class ThirdPartyDependencyListActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt index 87e71a94ddb..55d903219bd 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt @@ -45,13 +45,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -475,7 +478,9 @@ class ThirdPartyDependencyListFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt index a3cb999be4d..1a41429def0 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt @@ -42,13 +42,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -197,7 +200,9 @@ class TopicActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/TopicFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/TopicFragmentTest.kt index 551141b4af3..5f47407774b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/TopicFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/TopicFragmentTest.kt @@ -53,13 +53,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -652,7 +655,9 @@ class TopicFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt index 7de3ff0569c..0baa3f515a0 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt @@ -51,13 +51,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -427,7 +430,9 @@ class ConceptCardFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt index 2ab597a75d1..42cd11e3ed3 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt @@ -55,13 +55,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -483,7 +486,9 @@ class TopicInfoFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt index 2c4251fd0f2..18a483907d1 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt @@ -61,13 +61,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1000,7 +1003,9 @@ class TopicLessonsFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt index 56fa8cb4e05..ce2bebc8bf4 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt @@ -52,13 +52,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -424,7 +427,9 @@ class TopicPracticeFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityTest.kt index 6d3d747fe1d..c0a3f4595c0 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityTest.kt @@ -75,13 +75,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -719,7 +722,9 @@ class QuestionPlayerActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt index e8e933c21ce..ab3d409fece 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt @@ -53,13 +53,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -320,7 +323,9 @@ class TopicRevisionFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityTest.kt index d08e2a61010..2ae6f908dee 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityTest.kt @@ -43,13 +43,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -278,7 +281,9 @@ class RevisionCardActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt index 95f22c51025..8bff4e25299 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt @@ -61,13 +61,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -590,7 +593,9 @@ class RevisionCardFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/utility/RatioExtensionsTest.kt b/app/src/sharedTest/java/org/oppia/android/app/utility/RatioExtensionsTest.kt index 2260eace947..ff080b94a34 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/utility/RatioExtensionsTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/utility/RatioExtensionsTest.kt @@ -28,13 +28,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -143,7 +146,9 @@ class RatioExtensionsTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughActivityTest.kt index 9f4fcfa2482..cd00f8aa28b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughActivityTest.kt @@ -37,13 +37,16 @@ import org.oppia.android.app.utility.ProgressMatcher.Companion.withProgress import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -195,7 +198,9 @@ class WalkthroughActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt index 6cf37058cad..ee00467d323 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.ProgressMatcher.Companion.withProgress import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -280,7 +283,9 @@ class WalkthroughFinalFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughTopicListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughTopicListFragmentTest.kt index a535b248c50..21ff6100425 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughTopicListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughTopicListFragmentTest.kt @@ -45,13 +45,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -306,7 +309,9 @@ class WalkthroughTopicListFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughWelcomeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughWelcomeFragmentTest.kt index 849c5b61ce3..0a39884dd55 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughWelcomeFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughWelcomeFragmentTest.kt @@ -40,13 +40,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -203,7 +206,9 @@ class WalkthroughWelcomeFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/activity/ActivityIntentFactoriesTest.kt b/app/src/test/java/org/oppia/android/app/activity/ActivityIntentFactoriesTest.kt index 4331cd3e682..9a145af0a7e 100644 --- a/app/src/test/java/org/oppia/android/app/activity/ActivityIntentFactoriesTest.kt +++ b/app/src/test/java/org/oppia/android/app/activity/ActivityIntentFactoriesTest.kt @@ -31,13 +31,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -170,7 +173,9 @@ class ActivityIntentFactoriesTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class + ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt index b4cbbcd59d9..8e62d7024a1 100644 --- a/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt @@ -32,13 +32,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -143,7 +146,8 @@ class HomeActivityLocalTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt b/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt index 1d71c8c8afd..2e0be2a1d38 100644 --- a/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt +++ b/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt @@ -28,13 +28,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -479,7 +482,9 @@ class StringToFractionParserTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/parser/StringToRatioParserTest.kt b/app/src/test/java/org/oppia/android/app/parser/StringToRatioParserTest.kt index 57867f5f370..9ece48261e7 100644 --- a/app/src/test/java/org/oppia/android/app/parser/StringToRatioParserTest.kt +++ b/app/src/test/java/org/oppia/android/app/parser/StringToRatioParserTest.kt @@ -28,13 +28,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -256,7 +259,9 @@ class StringToRatioParserTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt index 6ff810db3a9..c721dce2972 100644 --- a/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt @@ -33,13 +33,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -211,7 +214,8 @@ class ExplorationActivityLocalTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt index dc0ff28e020..4dc01162509 100644 --- a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt @@ -87,13 +87,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -2061,7 +2064,9 @@ class StateFragmentLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/profile/ProfileChooserFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/profile/ProfileChooserFragmentLocalTest.kt index 3c9833ab88c..08f6ac552f3 100644 --- a/app/src/test/java/org/oppia/android/app/profile/ProfileChooserFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/profile/ProfileChooserFragmentLocalTest.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -133,7 +136,9 @@ class ProfileChooserFragmentLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/story/StoryActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/story/StoryActivityLocalTest.kt index 05458296564..ccb38b584c9 100644 --- a/app/src/test/java/org/oppia/android/app/story/StoryActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/story/StoryActivityLocalTest.kt @@ -31,13 +31,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -157,7 +160,9 @@ class StoryActivityLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/CompletedStoryListSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/CompletedStoryListSpanTest.kt index 1080c3c329b..d9d447ac23f 100644 --- a/app/src/test/java/org/oppia/android/app/testing/CompletedStoryListSpanTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/CompletedStoryListSpanTest.kt @@ -33,13 +33,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -165,7 +168,8 @@ class CompletedStoryListSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/HomeSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/HomeSpanTest.kt index 91d1cb2e664..842529cd959 100644 --- a/app/src/test/java/org/oppia/android/app/testing/HomeSpanTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/HomeSpanTest.kt @@ -33,13 +33,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -179,7 +182,8 @@ class HomeSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/OngoingTopicListSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/OngoingTopicListSpanTest.kt index 370944949d3..ce592d56c0e 100644 --- a/app/src/test/java/org/oppia/android/app/testing/OngoingTopicListSpanTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/OngoingTopicListSpanTest.kt @@ -34,13 +34,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -176,7 +179,8 @@ class OngoingTopicListSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt b/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt index 73932a62756..3cad9b897a2 100644 --- a/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt @@ -47,13 +47,16 @@ import org.oppia.android.data.backends.gae.OppiaRetrofit import org.oppia.android.data.backends.gae.RemoteAuthNetworkInterceptor import org.oppia.android.data.backends.gae.api.PlatformParameterService import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -349,7 +352,9 @@ class PlatformParameterIntegrationTest { ExplorationStorageModule::class, TestNetworkModule::class, RetrofitTestModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, - ActivityRecreatorTestModule::class, PlatformParameterSingletonModule::class + ActivityRecreatorTestModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/ProfileChooserSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/ProfileChooserSpanTest.kt index 83f154ff62d..9d428db0559 100644 --- a/app/src/test/java/org/oppia/android/app/testing/ProfileChooserSpanTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/ProfileChooserSpanTest.kt @@ -32,13 +32,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -378,7 +381,8 @@ class ProfileChooserSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/ProfileProgressSpanCount.kt b/app/src/test/java/org/oppia/android/app/testing/ProfileProgressSpanCount.kt index 247e6382b90..461919298a4 100644 --- a/app/src/test/java/org/oppia/android/app/testing/ProfileProgressSpanCount.kt +++ b/app/src/test/java/org/oppia/android/app/testing/ProfileProgressSpanCount.kt @@ -33,13 +33,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -162,7 +165,8 @@ class ProfileProgressSpanCount { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/RecentlyPlayedSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/RecentlyPlayedSpanTest.kt index 1f3325e4521..f6eeaa709ce 100644 --- a/app/src/test/java/org/oppia/android/app/testing/RecentlyPlayedSpanTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/RecentlyPlayedSpanTest.kt @@ -35,13 +35,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -297,7 +300,8 @@ class RecentlyPlayedSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/TopicRevisionSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/TopicRevisionSpanTest.kt index c4e56b6316a..931f5c69356 100644 --- a/app/src/test/java/org/oppia/android/app/testing/TopicRevisionSpanTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/TopicRevisionSpanTest.kt @@ -32,13 +32,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -162,7 +165,8 @@ class TopicRevisionSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/activity/TestActivityTest.kt b/app/src/test/java/org/oppia/android/app/testing/activity/TestActivityTest.kt index 915e262e512..b13424928ea 100644 --- a/app/src/test/java/org/oppia/android/app/testing/activity/TestActivityTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/activity/TestActivityTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -187,7 +190,9 @@ class TestActivityTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, ActivityRecreatorTestModule::class, LocaleProdModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/administratorcontrols/AdministratorControlsFragmentTest.kt b/app/src/test/java/org/oppia/android/app/testing/administratorcontrols/AdministratorControlsFragmentTest.kt index 0984dbbae3b..842475f0213 100644 --- a/app/src/test/java/org/oppia/android/app/testing/administratorcontrols/AdministratorControlsFragmentTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/administratorcontrols/AdministratorControlsFragmentTest.kt @@ -43,13 +43,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -222,7 +225,9 @@ class AdministratorControlsFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt b/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt index 7b3b36d0d01..41d04fdc19e 100644 --- a/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt @@ -42,13 +42,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -271,7 +274,9 @@ class OptionsFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/player/split/PlayerSplitScreenTesting.kt b/app/src/test/java/org/oppia/android/app/testing/player/split/PlayerSplitScreenTesting.kt index db50ed5a66b..2750f397034 100644 --- a/app/src/test/java/org/oppia/android/app/testing/player/split/PlayerSplitScreenTesting.kt +++ b/app/src/test/java/org/oppia/android/app/testing/player/split/PlayerSplitScreenTesting.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.utility.SplitScreenManager import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -190,7 +193,8 @@ class PlayerSplitScreenTesting { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt b/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt index b198682e10c..6cc58786788 100644 --- a/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt @@ -37,13 +37,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -204,7 +207,8 @@ class StateFragmentAccessibilityTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/topic/info/TopicInfoFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/topic/info/TopicInfoFragmentLocalTest.kt index 0d22b3a092d..131d7e63b57 100644 --- a/app/src/test/java/org/oppia/android/app/topic/info/TopicInfoFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/topic/info/TopicInfoFragmentLocalTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -145,7 +148,9 @@ class TopicInfoFragmentLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentLocalTest.kt index 534a0d63c58..51045169537 100644 --- a/app/src/test/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentLocalTest.kt @@ -28,13 +28,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -148,7 +151,9 @@ class TopicLessonsFragmentLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt index 71f96f4660d..42f7fad317e 100644 --- a/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt @@ -45,13 +45,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -411,7 +414,9 @@ class QuestionPlayerActivityLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityLocalTest.kt index eaa6ad78915..1cdc640a5ce 100644 --- a/app/src/test/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityLocalTest.kt @@ -28,13 +28,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -137,7 +140,9 @@ class RevisionCardActivityLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt b/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt index 9ae15728414..835f8d78d19 100644 --- a/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt +++ b/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt @@ -34,13 +34,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -512,7 +515,9 @@ class AppLanguageResourceHandlerTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleTestModule::class, ActivityRecreatorTestModule::class, - ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class + ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/translation/AppLanguageWatcherMixinTest.kt b/app/src/test/java/org/oppia/android/app/translation/AppLanguageWatcherMixinTest.kt index b86d62f1f51..6c368dad4eb 100644 --- a/app/src/test/java/org/oppia/android/app/translation/AppLanguageWatcherMixinTest.kt +++ b/app/src/test/java/org/oppia/android/app/translation/AppLanguageWatcherMixinTest.kt @@ -39,13 +39,16 @@ import org.oppia.android.app.translation.testing.TestActivityRecreator import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -260,7 +263,9 @@ class AppLanguageWatcherMixinTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleTestModule::class, ActivityRecreatorTestModule::class, - ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class + ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt b/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt index 3f4504a1045..6e92fd30212 100644 --- a/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt +++ b/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -174,7 +177,9 @@ class DateTimeUtilTest { HtmlParserEntityTypeModule::class, NetworkConnectionDebugUtilModule::class, DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/InteractionsModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/InteractionsModule.kt index 4175bda19eb..3dec6a50142 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/InteractionsModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/InteractionsModule.kt @@ -4,13 +4,16 @@ import dagger.Module import dagger.Provides import dagger.multibindings.IntoMap import dagger.multibindings.StringKey +import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules import org.oppia.android.domain.classify.rules.ContinueRules import org.oppia.android.domain.classify.rules.DragDropSortInputRules import org.oppia.android.domain.classify.rules.FractionInputRules import org.oppia.android.domain.classify.rules.ImageClickInputRules import org.oppia.android.domain.classify.rules.ItemSelectionInputRules +import org.oppia.android.domain.classify.rules.MathEquationInputRules import org.oppia.android.domain.classify.rules.MultipleChoiceInputRules import org.oppia.android.domain.classify.rules.NumberWithUnitsRules +import org.oppia.android.domain.classify.rules.NumericExpressionInputRules import org.oppia.android.domain.classify.rules.NumericInputRules import org.oppia.android.domain.classify.rules.RatioExpressionInputRules import org.oppia.android.domain.classify.rules.TextInputRules @@ -107,4 +110,32 @@ class InteractionsModule { ): InteractionClassifier { return GenericInteractionClassifier(ruleClassifiers) } + + @Provides + @IntoMap + @StringKey("NumericExpressionInput") + fun provideNumericExpressionInputInteractionClassifier( + @NumericExpressionInputRules ruleClassifiers: Map + ): InteractionClassifier { + return GenericInteractionClassifier(ruleClassifiers) + } + + @Provides + @IntoMap + @StringKey("AlgebraicExpressionInput") + fun provideAlgebraicExpressionInputInteractionClassifier( + @AlgebraicExpressionInputRules ruleClassifiers: + Map + ): InteractionClassifier { + return GenericInteractionClassifier(ruleClassifiers) + } + + @Provides + @IntoMap + @StringKey("MathEquationInput") + fun provideMathEquationInputInteractionClassifier( + @MathEquationInputRules ruleClassifiers: Map + ): InteractionClassifier { + return GenericInteractionClassifier(ruleClassifiers) + } } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/AnswerClassificationControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/AnswerClassificationControllerTest.kt index 2f682aee259..ff0fb19c488 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/AnswerClassificationControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/AnswerClassificationControllerTest.kt @@ -31,13 +31,16 @@ import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createReal import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createSetOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createString import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableSetOfNormalizedString +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -844,7 +847,9 @@ class AnswerClassificationControllerTest { ImageClickInputModule::class, RatioInputModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, LoggerModule::class, TestDispatcherModule::class, LogStorageModule::class, NetworkConnectionUtilDebugModule::class, - TestLogReportingModule::class, AssetModule::class, RobolectricModule::class + TestLogReportingModule::class, AssetModule::class, RobolectricModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent { diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt index 1c7d718ef49..361bedc9528 100644 --- a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt @@ -25,13 +25,16 @@ import org.oppia.android.app.model.EphemeralState import org.oppia.android.app.model.Exploration import org.oppia.android.app.model.ExplorationCheckpoint import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -351,7 +354,8 @@ class ExplorationDataControllerTest { RatioInputModule::class, RobolectricModule::class, FakeOppiaClockModule::class, TestExplorationStorageModule::class, HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, - AssetModule::class, LocaleProdModule::class + AssetModule::class, LocaleProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : DataProvidersInjector { diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt index a87412cf98b..337a0a98132 100644 --- a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt @@ -45,13 +45,16 @@ import org.oppia.android.app.model.TranslatableHtmlContentId import org.oppia.android.app.model.UserAnswer import org.oppia.android.app.model.WrittenTranslationLanguageSelection import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -3618,7 +3621,8 @@ class ExplorationProgressControllerTest { RatioInputModule::class, RobolectricModule::class, FakeOppiaClockModule::class, TestExplorationStorageModule::class, HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, - AssetModule::class, LocaleProdModule::class + AssetModule::class, LocaleProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : DataProvidersInjector { diff --git a/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt index c914d63f58e..5539f784c4f 100644 --- a/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt @@ -38,13 +38,16 @@ import org.oppia.android.app.model.UserAnswer import org.oppia.android.app.model.UserAssessmentPerformance import org.oppia.android.app.model.WrittenTranslationLanguageSelection import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1832,7 +1835,8 @@ class QuestionAssessmentProgressControllerTest { RatioInputModule::class, RobolectricModule::class, FakeOppiaClockModule::class, CachingTestModule::class, HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, - AssetModule::class, LocaleProdModule::class + AssetModule::class, LocaleProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : DataProvidersInjector { diff --git a/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt index 5f592a50171..f116d87232d 100644 --- a/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt @@ -23,13 +23,16 @@ import org.mockito.junit.MockitoRule import org.oppia.android.app.model.EphemeralQuestion import org.oppia.android.app.model.ProfileId import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -329,7 +332,9 @@ class QuestionTrainingControllerTest { LogStorageModule::class, TestDispatcherModule::class, RatioInputModule::class, RobolectricModule::class, FakeOppiaClockModule::class, CachingTestModule::class, HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, - NetworkConnectionUtilDebugModule::class, AssetModule::class, LocaleProdModule::class + NetworkConnectionUtilDebugModule::class, AssetModule::class, LocaleProdModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : DataProvidersInjector { diff --git a/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt b/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt index 3e4cc2d9011..498b1f36e04 100644 --- a/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt +++ b/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt @@ -16,13 +16,16 @@ import org.oppia.android.app.topic.PracticeTabModule import org.oppia.android.app.translation.ActivityRecreatorProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -86,6 +89,8 @@ import javax.inject.Singleton DeveloperOptionsModule::class, PlatformParameterSyncUpWorkerModule::class, NetworkConnectionUtilDebugModule::class, EndToEndTestNetworkConfigModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorProdModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class, // TODO(#59): Remove this module once we completely migrate to Bazel from Gradle as we can then // directly exclude debug files from the build and thus won't be requiring this module. NetworkConnectionDebugUtilModule::class diff --git a/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleCustomContextTest.kt b/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleCustomContextTest.kt index cd6da6c4408..e434ea51251 100644 --- a/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleCustomContextTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleCustomContextTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -253,7 +256,9 @@ class InitializeDefaultLocaleRuleCustomContextTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, ActivityRecreatorTestModule::class, LocaleProdModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleOmissionTest.kt b/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleOmissionTest.kt index bbade8b3c62..65c8900a1a0 100644 --- a/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleOmissionTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleOmissionTest.kt @@ -26,13 +26,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -129,7 +132,9 @@ class InitializeDefaultLocaleRuleOmissionTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, ActivityRecreatorTestModule::class, LocaleProdModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleTest.kt b/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleTest.kt index 437fcfc545d..6846fd3c69a 100644 --- a/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleTest.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -133,7 +136,9 @@ class InitializeDefaultLocaleRuleTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, ActivityRecreatorTestModule::class, LocaleProdModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { From 10db2ce6cd65b93393ca57d101963cfab1f0f554 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 23:16:44 -0800 Subject: [PATCH 125/289] Add a11y string generation for math expressions. This is mostly copied from #2173. --- app/BUILD.bazel | 4 + .../android/app/utility/math/BUILD.bazel | 22 + .../math/MathExpressionAccessibilityUtil.kt | 196 +++++ .../MathExpressionAccessibilityUtilTest.kt | 724 ++++++++++++++++++ .../testing/math/MathEquationSubject.kt | 2 +- .../testing/math/MathExpressionSubject.kt | 2 +- 6 files changed, 948 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel create mode 100644 app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt create mode 100644 app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 3e09ece2557..80d5ca54a6d 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -801,6 +801,7 @@ TEST_DEPS = [ ":test_deps", "//app/src/main/java/org/oppia/android/app/testing/activity:test_activity", "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", + "//app/src/main/java/org/oppia/android/app/utility/math:math_expression_accessibility_util", "//domain", "//domain/src/main/java/org/oppia/android/domain/audio:audio_player_controller", "//domain/src/main/java/org/oppia/android/domain/onboarding/testing:fake_exploration_meta_data_retriever", @@ -813,6 +814,8 @@ TEST_DEPS = [ "//testing/src/main/java/org/oppia/android/testing/espresso:konfetti_view_matcher", "//testing/src/main/java/org/oppia/android/testing/espresso:text_input_action", "//testing/src/main/java/org/oppia/android/testing/junit:initialize_default_locale_rule", + "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", + "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", "//testing/src/main/java/org/oppia/android/testing/mockito", "//testing/src/main/java/org/oppia/android/testing/network", "//testing/src/main/java/org/oppia/android/testing/network:test_module", @@ -848,6 +851,7 @@ TEST_DEPS = [ "//utility/src/main/java/org/oppia/android/util/accessibility:test_module", "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module", "//utility/src/main/java/org/oppia/android/util/caching/testing:caching_test_module", + "//utility/src/main/java/org/oppia/android/util/math:parser", ] # App module tests. Note that all tests are assumed to be tests with resources (even though not all diff --git a/app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel b/app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel new file mode 100644 index 00000000000..5b5ee7cb433 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel @@ -0,0 +1,22 @@ +""" +General purposes utilities corresponding to displaying math expressions & constructs. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "math_expression_accessibility_util", + srcs = [ + "MathExpressionAccessibilityUtil.kt", + ], + visibility = ["//app:app_visibility"], + deps = [ + ":dagger", + "//model/src/main/proto:languages_java_proto_lite", + "//model/src/main/proto:math_java_proto_lite", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + ], +) + +dagger_rules() diff --git a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt new file mode 100644 index 00000000000..c353b625048 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt @@ -0,0 +1,196 @@ +package org.oppia.android.app.utility.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall.FunctionType +import org.oppia.android.app.model.MathFunctionCall.FunctionType.FUNCTION_UNSPECIFIED +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.OppiaLanguage +import org.oppia.android.app.model.OppiaLanguage.ARABIC +import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.ENGLISH +import org.oppia.android.app.model.OppiaLanguage.HINDI +import org.oppia.android.app.model.OppiaLanguage.HINGLISH +import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED +import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED +import org.oppia.android.app.model.Real.RealTypeCase.INTEGER +import org.oppia.android.util.math.toPlainText +import java.text.NumberFormat +import java.util.Locale +import javax.inject.Inject +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator + +class MathExpressionAccessibilityUtil @Inject constructor() { + fun convertToHumanReadableString( + equation: MathEquation, + language: OppiaLanguage, + divAsFraction: Boolean + ): String? { + return when (language) { + ENGLISH -> equation.toHumanReadableEnglishString(divAsFraction) + ARABIC, HINDI, HINGLISH, PORTUGUESE, BRAZILIAN_PORTUGUESE, LANGUAGE_UNSPECIFIED, + UNRECOGNIZED -> null + } + } + + fun convertToHumanReadableString( + expression: MathExpression, + language: OppiaLanguage, + divAsFraction: Boolean + ): String? { + return when (language) { + ENGLISH -> expression.toHumanReadableEnglishString(divAsFraction) + ARABIC, HINDI, HINGLISH, PORTUGUESE, BRAZILIAN_PORTUGUESE, LANGUAGE_UNSPECIFIED, + UNRECOGNIZED -> null + } + } + + private companion object { + // TODO: move these to the UI layer & have them utilize non-translatable strings. + private val numberFormat by lazy { NumberFormat.getNumberInstance(Locale.US) } + private val singularOrdinalNames = mapOf( + 1 to "oneth", + 2 to "half", + 3 to "third", + 4 to "fourth", + 5 to "fifth", + 6 to "sixth", + 7 to "seventh", + 8 to "eighth", + 9 to "ninth", + 10 to "tenth", + ) + private val pluralOrdinalNames = mapOf( + 1 to "oneths", + 2 to "halves", + 3 to "thirds", + 4 to "fourths", + 5 to "fifths", + 6 to "sixths", + 7 to "sevenths", + 8 to "eighths", + 9 to "ninths", + 10 to "tenths", + ) + + private fun MathEquation.toHumanReadableEnglishString(divAsFraction: Boolean): String? { + val lhsStr = leftSide.toHumanReadableEnglishString(divAsFraction) + val rhsStr = rightSide.toHumanReadableEnglishString(divAsFraction) + return if (lhsStr != null && rhsStr != null) "$lhsStr equals $rhsStr" else null + } + + private fun MathExpression.toHumanReadableEnglishString(divAsFraction: Boolean): String? { + // Reference: + // https://docs.google.com/document/d/1P-dldXQ08O-02ZRG978paiWOSz0dsvcKpDgiV_rKH_Y/view. + return when (expressionTypeCase) { + CONSTANT -> if (constant.realTypeCase == INTEGER) { + numberFormat.format(constant.integer.toLong()) + } else constant.toPlainText() + VARIABLE -> when (variable) { + "z" -> "zed" + "Z" -> "Zed" + else -> variable + } + BINARY_OPERATION -> { + val lhs = binaryOperation.leftOperand + val rhs = binaryOperation.rightOperand + val lhsStr = lhs.toHumanReadableEnglishString(divAsFraction) + val rhsStr = rhs.toHumanReadableEnglishString(divAsFraction) + if (lhsStr == null || rhsStr == null) return null + when (binaryOperation.operator) { + ADD -> "$lhsStr plus $rhsStr" + SUBTRACT -> "$lhsStr minus $rhsStr" + MULTIPLY -> { + if (binaryOperation.canBeReadAsImplicitMultiplication()) { + "$lhsStr $rhsStr" + } else "$lhsStr times $rhsStr" + } + DIVIDE -> { + if (divAsFraction && lhs.isConstantInteger() && rhs.isConstantInteger()) { + val numerator = lhs.constant.integer + val denominator = rhs.constant.integer + if (numerator in 0..10 && denominator in 1..10 && denominator >= numerator) { + val ordinalName = + if (numerator == 1) { + singularOrdinalNames.getValue(denominator) + } else pluralOrdinalNames.getValue(denominator) + "$numerator $ordinalName" + } else "$lhsStr over $rhsStr" + } else if (divAsFraction) { + "the fraction with numerator $lhsStr and denominator $rhsStr" + } else "$lhsStr divided by $rhsStr" + } + EXPONENTIATE -> "$lhsStr raised to the power of $rhsStr" + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null + } + } + UNARY_OPERATION -> { + val operandStr = unaryOperation.operand.toHumanReadableEnglishString(divAsFraction) + when (unaryOperation.operator) { + NEGATE -> operandStr?.let { "negative $it" } + POSITIVE -> operandStr?.let { "positive $it" } + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> null + } + } + FUNCTION_CALL -> { + val argStr = functionCall.argument.toHumanReadableEnglishString(divAsFraction) + when (functionCall.functionType) { + SQUARE_ROOT -> argStr?.let { + if (functionCall.argument.isSingleTerm()) { + "square root of $it" + } else "start square root $it end square root" + } + FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> null + } + } + GROUP -> group.toHumanReadableEnglishString(divAsFraction)?.let { + if (isSingleTerm()) it else "open parenthesis $it close parenthesis" + } + EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathBinaryOperation.canBeReadAsImplicitMultiplication(): Boolean { + // Note that exponentiation is specialized since it's higher precedence than multiplication + // which means the graph won't look like "constant * variable" for polynomial terms like 2x^4 + // (which are cases the system should read using implicit multiplication, e.g. "two x raised + // to the power of 4"). + if (!isImplicit || !leftOperand.isConstant()) return false + return rightOperand.isVariable() || rightOperand.isExponentiation() + } + + private fun MathExpression.isConstantInteger(): Boolean = + expressionTypeCase == CONSTANT && constant.realTypeCase == INTEGER + + private fun MathExpression.isConstant(): Boolean = expressionTypeCase == CONSTANT + + private fun MathExpression.isVariable(): Boolean = expressionTypeCase == VARIABLE + + private fun MathExpression.isExponentiation(): Boolean = + expressionTypeCase == BINARY_OPERATION && binaryOperation.operator == EXPONENTIATE + + private fun MathExpression.isSingleTerm(): Boolean = when (expressionTypeCase) { + CONSTANT, VARIABLE, FUNCTION_CALL -> true + BINARY_OPERATION, UNARY_OPERATION -> false + GROUP -> group.isSingleTerm() + EXPRESSIONTYPE_NOT_SET, null -> false + } + } +} diff --git a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt new file mode 100644 index 00000000000..6ada50c67ff --- /dev/null +++ b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt @@ -0,0 +1,724 @@ +package org.oppia.android.app.utility.math + +import android.app.Application +import androidx.appcompat.app.AppCompatActivity +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.StringSubject +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.activity.ActivityComponent +import org.oppia.android.app.activity.ActivityComponentFactory +import org.oppia.android.app.application.ApplicationComponent +import org.oppia.android.app.application.ApplicationInjector +import org.oppia.android.app.application.ApplicationInjectorProvider +import org.oppia.android.app.application.ApplicationModule +import org.oppia.android.app.application.ApplicationStartupListenerModule +import org.oppia.android.app.devoptions.DeveloperOptionsModule +import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.OppiaLanguage +import org.oppia.android.app.model.OppiaLanguage.ARABIC +import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.ENGLISH +import org.oppia.android.app.model.OppiaLanguage.HINDI +import org.oppia.android.app.model.OppiaLanguage.HINGLISH +import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED +import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED +import org.oppia.android.app.topic.PracticeTabModule +import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule +import org.oppia.android.data.backends.gae.NetworkConfigProdModule +import org.oppia.android.data.backends.gae.NetworkModule +import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule +import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule +import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule +import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule +import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.android.domain.oppialogger.LogStorageModule +import org.oppia.android.domain.platformparameter.PlatformParameterModule +import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule +import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule +import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.math.MathEquationSubject +import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat +import org.oppia.android.testing.math.MathExpressionSubject +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.accessibility.AccessibilityTestModule +import org.oppia.android.util.caching.AssetModule +import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.gcsresource.GcsResourceModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.EnableConsoleLog +import org.oppia.android.util.logging.EnableFileLog +import org.oppia.android.util.logging.GlobalLogLevel +import org.oppia.android.util.logging.LogLevel +import org.oppia.android.util.math.MathExpressionParser +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule +import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule +import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule +import org.oppia.android.util.parser.image.GlideImageLoaderModule +import org.oppia.android.util.parser.image.ImageParsingModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton + +/** Tests for [MathExpressionAccessibilityUtil]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(application = MathExpressionAccessibilityUtilTest.TestApplication::class) +class MathExpressionAccessibilityUtilTest { + @Inject lateinit var util: MathExpressionAccessibilityUtil + + @Before + fun setUp() { + setUpTestApplicationComponent() + } + + @Test + fun testHumanReadableString() { + // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). + + val exp1 = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp1).forHumanReadable(ARABIC).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(HINDI).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(HINGLISH).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(PORTUGUESE).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + + val exp2 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp2).forHumanReadable(ARABIC).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(HINDI).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(HINGLISH).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(PORTUGUESE).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + + val eq1 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") + assertThat(eq1).forHumanReadable(ARABIC).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(HINDI).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(HINGLISH).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(PORTUGUESE).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + + // specific cases (from rules & other cases): + val exp3 = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp3).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + assertThat(exp3).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp49 = parseNumericExpressionSuccessfullyWithAllErrors("-1") + assertThat(exp49).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative 1") + + val exp50 = parseNumericExpressionSuccessfullyWithAllErrors("+1") + assertThat(exp50).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive 1") + + val exp4 = parseNumericExpressionSuccessfullyWithoutOptionalErrors("((1))") + assertThat(exp4).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp5 = parseNumericExpressionSuccessfullyWithAllErrors("1+2") + assertThat(exp5).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus 2") + + val exp6 = parseNumericExpressionSuccessfullyWithAllErrors("1-2") + assertThat(exp6).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus 2") + + val exp7 = parseNumericExpressionSuccessfullyWithAllErrors("1*2") + assertThat(exp7).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times 2") + + val exp8 = parseNumericExpressionSuccessfullyWithAllErrors("1/2") + assertThat(exp8).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by 2") + + val exp9 = parseNumericExpressionSuccessfullyWithAllErrors("1+(1-2)") + assertThat(exp9) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("1 plus open parenthesis 1 minus 2 close parenthesis") + + val exp10 = parseNumericExpressionSuccessfullyWithAllErrors("2^3") + assertThat(exp10) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("2 raised to the power of 3") + + val exp11 = parseNumericExpressionSuccessfullyWithAllErrors("2^(1+2)") + assertThat(exp11) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("2 raised to the power of open parenthesis 1 plus 2 close parenthesis") + + val exp12 = parseNumericExpressionSuccessfullyWithAllErrors("100000*2") + assertThat(exp12).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") + + val exp13 = parseNumericExpressionSuccessfullyWithAllErrors("sqrt(2)") + assertThat(exp13).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + + val exp14 = parseNumericExpressionSuccessfullyWithAllErrors("√2") + assertThat(exp14).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + + val exp15 = parseNumericExpressionSuccessfullyWithAllErrors("sqrt(1+2)") + assertThat(exp15) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root 1 plus 2 end square root") + + val singularOrdinalNames = mapOf( + 1 to "oneth", + 2 to "half", + 3 to "third", + 4 to "fourth", + 5 to "fifth", + 6 to "sixth", + 7 to "seventh", + 8 to "eighth", + 9 to "ninth", + 10 to "tenth", + ) + val pluralOrdinalNames = mapOf( + 1 to "oneths", + 2 to "halves", + 3 to "thirds", + 4 to "fourths", + 5 to "fifths", + 6 to "sixths", + 7 to "sevenths", + 8 to "eighths", + 9 to "ninths", + 10 to "tenths", + ) + for (denominatorToCheck in 1..10) { + for (numeratorToCheck in 0..denominatorToCheck) { + val exp16 = + parseNumericExpressionSuccessfullyWithAllErrors("$numeratorToCheck/$denominatorToCheck") + + val ordinalName = + if (numeratorToCheck == 1) { + singularOrdinalNames.getValue(denominatorToCheck) + } else pluralOrdinalNames.getValue(denominatorToCheck) + assertThat(exp16) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("$numeratorToCheck $ordinalName") + } + } + + val exp17 = parseNumericExpressionSuccessfullyWithAllErrors("-1/3") + assertThat(exp17) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("negative 1 third") + + val exp18 = parseNumericExpressionSuccessfullyWithAllErrors("-2/3") + assertThat(exp18) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("negative 2 thirds") + + val exp19 = parseNumericExpressionSuccessfullyWithAllErrors("10/11") + assertThat(exp19) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("10 over 11") + + val exp20 = parseNumericExpressionSuccessfullyWithAllErrors("121/7986") + assertThat(exp20) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("121 over 7,986") + + val exp21 = parseNumericExpressionSuccessfullyWithAllErrors("8/7") + assertThat(exp21) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("8 over 7") + + val exp22 = parseNumericExpressionSuccessfullyWithAllErrors("-10/-30") + assertThat(exp22) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("negative the fraction with numerator 10 and denominator negative 30") + + val exp23 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1") + assertThat(exp23).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp24 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("((1))") + assertThat(exp24).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp25 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp25).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") + + val exp26 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("((x))") + assertThat(exp26).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") + + val exp51 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-x") + assertThat(exp51).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative x") + + val exp52 = parseAlgebraicExpressionSuccessfullyWithAllErrors("+x") + assertThat(exp52).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive x") + + val exp27 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x") + assertThat(exp27).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus x") + + val exp28 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1-x") + assertThat(exp28).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus x") + + val exp29 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1*x") + assertThat(exp29).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times x") + + val exp30 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1/x") + assertThat(exp30).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by x") + + val exp31 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1/x") + assertThat(exp31) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("the fraction with numerator 1 and denominator x") + + val exp32 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(1-x)") + assertThat(exp32) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("1 plus open parenthesis 1 minus x close parenthesis") + + val exp33 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2x") + assertThat(exp33).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x") + + val exp34 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy") + assertThat(exp34).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x times y") + + val exp35 = parseAlgebraicExpressionSuccessfullyWithAllErrors("z") + assertThat(exp35).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("zed") + + val exp36 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xz") + assertThat(exp36).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x times zed") + + val exp37 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2") + assertThat(exp37) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x raised to the power of 2") + + val exp38 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("x^(1+x)") + assertThat(exp38) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x raised to the power of open parenthesis 1 plus x close parenthesis") + + val exp39 = parseAlgebraicExpressionSuccessfullyWithAllErrors("100000*2") + assertThat(exp39).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") + + val exp40 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(2)") + assertThat(exp40).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + + val exp41 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(x)") + assertThat(exp41).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") + + val exp42 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√2") + assertThat(exp42).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + + val exp43 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√x") + assertThat(exp43).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") + + val exp44 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(1+2)") + assertThat(exp44) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root 1 plus 2 end square root") + + val exp45 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(1+x)") + assertThat(exp45) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root 1 plus x end square root") + + val exp46 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(1+x)") + assertThat(exp46) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root open parenthesis 1 plus x close parenthesis end square root") + + for (denominatorToCheck in 1..10) { + for (numeratorToCheck in 0..denominatorToCheck) { + val exp16 = + parseAlgebraicExpressionSuccessfullyWithAllErrors("$numeratorToCheck/$denominatorToCheck") + + val ordinalName = + if (numeratorToCheck == 1) { + singularOrdinalNames.getValue(denominatorToCheck) + } else pluralOrdinalNames.getValue(denominatorToCheck) + assertThat(exp16) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("$numeratorToCheck $ordinalName") + } + } + + val exp47 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1") + assertThat(exp47).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp48 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x(5-y)") + assertThat(exp48) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x times open parenthesis 5 minus y close parenthesis") + + val eq2 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/y") + assertThat(eq2) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x equals 1 divided by y") + + val eq3 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/2") + assertThat(eq3) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x equals 1 divided by 2") + + val eq4 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/y") + assertThat(eq4) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("x equals the fraction with numerator 1 and denominator y") + + val eq5 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/2") + assertThat(eq5) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("x equals 1 half") + + // Tests from examples in the PRD + val eq6 = parseAlgebraicEquationSuccessfullyWithAllErrors("3x^2+4y=62") + assertThat(eq6) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("3 x raised to the power of 2 plus 4 y equals 62") + + val exp53 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x+6)/(x-4)") + assertThat(exp53) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo( + "the fraction with numerator open parenthesis x plus 6 close parenthesis and denominator" + + " open parenthesis x minus 4 close parenthesis" + ) + + val exp54 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("4*(x)^(2)+20x") + assertThat(exp54) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("4 times x raised to the power of 2 plus 20 x") + + val exp55 = parseAlgebraicExpressionSuccessfullyWithAllErrors("3+x-5") + assertThat(exp55).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("3 plus x minus 5") + + val exp56 = + parseAlgebraicExpressionSuccessfullyWithAllErrors( + "Z+A-Z", allowedVariables = listOf("A", "Z") + ) + assertThat(exp56).forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("Zed plus A minus Zed") + + val exp57 = + parseAlgebraicExpressionSuccessfullyWithAllErrors( + "6C-5A-1", allowedVariables = listOf("A", "C") + ) + assertThat(exp57) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("6 C minus 5 A minus 1") + + val exp58 = + parseAlgebraicExpressionSuccessfullyWithAllErrors( + "5*Z-w", allowedVariables = listOf("Z", "w") + ) + assertThat(exp58) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("5 times Zed minus w") + + val exp59 = + parseAlgebraicExpressionSuccessfullyWithAllErrors( + "L*S-3S+L", allowedVariables = listOf("L", "S") + ) + assertThat(exp59) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("L times S minus 3 S plus L") + + val exp60 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(2+6+3+4)") + assertThat(exp60) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("2 times open parenthesis 2 plus 6 plus 3 plus 4 close parenthesis") + + val exp61 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(64)") + assertThat(exp61) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("square root of 64") + + val exp62 = + parseAlgebraicExpressionSuccessfullyWithAllErrors( + "√(a+b)", allowedVariables = listOf("a", "b") + ) + assertThat(exp62) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root open parenthesis a plus b close parenthesis end square root") + + val exp63 = parseAlgebraicExpressionSuccessfullyWithAllErrors("3*10^-5") + assertThat(exp63) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("3 times 10 raised to the power of negative 5") + + val exp64 = + parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( + "((x+2y)+5*(a-2b)+z)", allowedVariables = listOf("x", "y", "a", "b", "z") + ) + assertThat(exp64) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo( + "open parenthesis open parenthesis x plus 2 y close parenthesis plus 5 times open" + + " parenthesis a minus 2 b close parenthesis plus zed close parenthesis" + ) + } + + private fun MathExpressionSubject.forHumanReadable( + language: OppiaLanguage + ): HumanReadableStringChecker { + return HumanReadableStringChecker(language) { divAsFraction -> + util.convertToHumanReadableString(actual, language, divAsFraction) + } + } + + private fun MathEquationSubject.forHumanReadable( + language: OppiaLanguage + ): HumanReadableStringChecker { + return HumanReadableStringChecker(language) { divAsFraction -> + util.convertToHumanReadableString(actual, language, divAsFraction) + } + } + + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + + private class HumanReadableStringChecker( + private val language: OppiaLanguage, + private val maybeConvertToHumanReadableString: (Boolean) -> String? + ) { + fun convertsToStringThat(): StringSubject = + assertThat(convertToHumanReadableString(language, /* divAsFraction= */ false)) + + fun convertsWithFractionsToStringThat(): StringSubject = + assertThat(convertToHumanReadableString(language, /* divAsFraction= */ true)) + + fun doesNotConvertToString() { + assertWithMessage("Expected to not convert to: $language") + .that(maybeConvertToHumanReadableString(/* divAsFraction= */ false)) + .isNull() + } + + private fun convertToHumanReadableString( + language: OppiaLanguage, + divAsFraction: Boolean + ): String { + val readableString = maybeConvertToHumanReadableString(divAsFraction) + assertWithMessage("Expected to convert to: $language").that(readableString).isNotNull() + return checkNotNull(readableString) // Verified in the above assertion check. + } + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + // TODO(#59): Either isolate these to their own shared test module, or use the real logging + // module in tests to avoid needing to specify these settings for tests. + @EnableConsoleLog + @Provides + fun provideEnableConsoleLog(): Boolean = true + + @EnableFileLog + @Provides + fun provideEnableFileLog(): Boolean = false + + @GlobalLogLevel + @Provides + fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, RobolectricModule::class, FakeOppiaClockModule::class, + TestLogReportingModule::class, TestDispatcherModule::class, ApplicationModule::class, + ApplicationStartupListenerModule::class, WorkManagerConfigurationModule::class, + ImageParsingModule::class, AccessibilityTestModule::class, PracticeTabModule::class, + GcsResourceModule::class, NetworkConnectionUtilDebugModule::class, LogStorageModule::class, + NetworkModule::class, PlatformParameterModule::class, HintsAndSolutionProdModule::class, + CachingTestModule::class, InteractionsModule::class, ExplorationStorageModule::class, + QuestionModule::class, NetworkConfigProdModule::class, ContinueModule::class, + FractionInputModule::class, ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, RatioInputModule::class, + HintsAndSolutionConfigModule::class, ExpirationMetaDataRetrieverModule::class, + GlideImageLoaderModule::class, PrimeTopicAssetsControllerModule::class, + HtmlParserEntityTypeModule::class, NetworkConnectionDebugUtilModule::class, + DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, AssetModule::class, + LocaleProdModule::class, ActivityRecreatorTestModule::class, + PlatformParameterSingletonModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class + ] + ) + interface TestApplicationComponent : ApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: MathExpressionAccessibilityUtilTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerMathExpressionAccessibilityUtilTest_TestApplicationComponent.builder() + .setApplication(this) + .build() + } + + fun inject(test: MathExpressionAccessibilityUtilTest) { + component.inject(test) + } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component + } + + private companion object { + private fun parseNumericExpressionSuccessfullyWithAllErrors( + expression: String + ): MathExpression { + val result = parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionSuccessfullyWithoutOptionalErrors( + expression: String + ): MathExpression { + val result = + parseNumericExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY + ) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode + ): MathParsingResult { + return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) + } + + private fun parseAlgebraicExpressionSuccessfullyWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) + } + + private fun parseAlgebraicEquationSuccessfullyWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathEquation { + val result = + MathExpressionParser.parseAlgebraicEquation( + expression, allowedVariables, + ErrorCheckingMode.ALL_ERRORS + ) + return (result as MathParsingResult.Success).result + } + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt index 373b1434b0e..3e4b44e7449 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt @@ -11,7 +11,7 @@ import org.oppia.android.util.math.toRawLatex class MathEquationSubject( metadata: FailureMetadata, - private val actual: MathEquation + val actual: MathEquation ) : LiteProtoSubject(metadata, actual) { fun hasLeftHandSideThat(): MathExpressionSubject = assertThat(actual.leftSide) diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt index eea079a7e4b..b8e816fb60e 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt @@ -25,7 +25,7 @@ import org.oppia.android.util.math.toRawLatex // See: https://kotlinlang.org/docs/type-safe-builders.html. class MathExpressionSubject( metadata: FailureMetadata, - private val actual: MathExpression + val actual: MathExpression ) : LiteProtoSubject(metadata, actual) { fun hasStructureThatMatches(init: ExpressionComparator.() -> Unit) { // TODO: maybe verify that all aspects are verified? From 9026f9bdddb762612c5f48e4424ef056f3d8152c Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 23:35:59 -0800 Subject: [PATCH 126/289] Remove dead code. --- .../util/math/MathExpressionExtensions.kt | 174 ------------------ 1 file changed, 174 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 785c24e8079..c05328d2f65 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -1,7 +1,6 @@ package org.oppia.android.util.math import org.oppia.android.app.model.ComparableOperationList -import org.oppia.android.app.model.MathBinaryOperation import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION @@ -11,182 +10,16 @@ import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CA import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE -import org.oppia.android.app.model.MathFunctionCall.FunctionType -import org.oppia.android.app.model.OppiaLanguage -import org.oppia.android.app.model.OppiaLanguage.ARABIC -import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE -import org.oppia.android.app.model.OppiaLanguage.ENGLISH -import org.oppia.android.app.model.OppiaLanguage.HINDI -import org.oppia.android.app.model.OppiaLanguage.HINGLISH -import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED -import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE -import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Real -import org.oppia.android.app.model.Real.RealTypeCase.INTEGER -import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL -import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL -import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import org.oppia.android.util.math.ExpressionToComparableOperationListConverter.Companion.toComparable import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex import org.oppia.android.util.math.ExpressionToPolynomialConverter.Companion.reduceToPolynomial import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate -import java.text.NumberFormat -import java.util.Locale -import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator -import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator - -// TODO: split up this extensions file into multiple, clean it up, reorganize, and add tests. fun MathExpression.toComparableOperationList(): ComparableOperationList = stripGroups().toComparable() -// TODO: move these to the UI layer & have them utilize non-translatable strings. -private val numberFormat by lazy { NumberFormat.getNumberInstance(Locale.US) } -private val singularOrdinalNames = mapOf( - 1 to "oneth", - 2 to "half", - 3 to "third", - 4 to "fourth", - 5 to "fifth", - 6 to "sixth", - 7 to "seventh", - 8 to "eighth", - 9 to "ninth", - 10 to "tenth", -) -private val pluralOrdinalNames = mapOf( - 1 to "oneths", - 2 to "halves", - 3 to "thirds", - 4 to "fourths", - 5 to "fifths", - 6 to "sixths", - 7 to "sevenths", - 8 to "eighths", - 9 to "ninths", - 10 to "tenths", -) - -fun MathEquation.toHumanReadableString(language: OppiaLanguage, divAsFraction: Boolean): String? { - return when (language) { - ENGLISH -> toHumanReadableEnglishString(divAsFraction) - ARABIC, HINDI, HINGLISH, PORTUGUESE, BRAZILIAN_PORTUGUESE, LANGUAGE_UNSPECIFIED, UNRECOGNIZED -> - null - } -} - -fun MathExpression.toHumanReadableString(language: OppiaLanguage, divAsFraction: Boolean): String? { - return when (language) { - ENGLISH -> toHumanReadableEnglishString(divAsFraction) - ARABIC, HINDI, HINGLISH, PORTUGUESE, BRAZILIAN_PORTUGUESE, LANGUAGE_UNSPECIFIED, UNRECOGNIZED -> - null - } -} - -private fun MathEquation.toHumanReadableEnglishString(divAsFraction: Boolean): String? { - val lhsStr = leftSide.toHumanReadableEnglishString(divAsFraction) - val rhsStr = rightSide.toHumanReadableEnglishString(divAsFraction) - return if (lhsStr != null && rhsStr != null) "$lhsStr equals $rhsStr" else null -} - -private fun MathExpression.toHumanReadableEnglishString(divAsFraction: Boolean): String? { - // Reference: - // https://docs.google.com/document/d/1P-dldXQ08O-02ZRG978paiWOSz0dsvcKpDgiV_rKH_Y/view. - return when (expressionTypeCase) { - CONSTANT -> if (constant.realTypeCase == INTEGER) { - numberFormat.format(constant.integer.toLong()) - } else constant.toPlainString() - VARIABLE -> when (variable) { - "z" -> "zed" - "Z" -> "Zed" - else -> variable - } - BINARY_OPERATION -> { - val lhs = binaryOperation.leftOperand - val rhs = binaryOperation.rightOperand - val lhsStr = lhs.toHumanReadableEnglishString(divAsFraction) - val rhsStr = rhs.toHumanReadableEnglishString(divAsFraction) - if (lhsStr == null || rhsStr == null) return null - when (binaryOperation.operator) { - BinaryOperator.ADD -> "$lhsStr plus $rhsStr" - BinaryOperator.SUBTRACT -> "$lhsStr minus $rhsStr" - BinaryOperator.MULTIPLY -> { - if (binaryOperation.canBeReadAsImplicitMultiplication()) { - "$lhsStr $rhsStr" - } else "$lhsStr times $rhsStr" - } - BinaryOperator.DIVIDE -> { - if (divAsFraction && lhs.isConstantInteger() && rhs.isConstantInteger()) { - val numerator = lhs.constant.integer - val denominator = rhs.constant.integer - if (numerator in 0..10 && denominator in 1..10 && denominator >= numerator) { - val ordinalName = - if (numerator == 1) { - singularOrdinalNames.getValue(denominator) - } else pluralOrdinalNames.getValue(denominator) - "$numerator $ordinalName" - } else "$lhsStr over $rhsStr" - } else if (divAsFraction) { - "the fraction with numerator $lhsStr and denominator $rhsStr" - } else "$lhsStr divided by $rhsStr" - } - BinaryOperator.EXPONENTIATE -> "$lhsStr raised to the power of $rhsStr" - BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null - } - } - UNARY_OPERATION -> { - val operandStr = unaryOperation.operand.toHumanReadableEnglishString(divAsFraction) - when (unaryOperation.operator) { - UnaryOperator.NEGATE -> operandStr?.let { "negative $it" } - UnaryOperator.POSITIVE -> operandStr?.let { "positive $it" } - UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> null - } - } - FUNCTION_CALL -> { - val argStr = functionCall.argument.toHumanReadableEnglishString(divAsFraction) - when (functionCall.functionType) { - FunctionType.SQUARE_ROOT -> argStr?.let { - if (functionCall.argument.isSingleTerm()) { - "square root of $it" - } else "start square root $it end square root" - } - FunctionType.FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> null - } - } - GROUP -> group.toHumanReadableEnglishString(divAsFraction)?.let { - if (isSingleTerm()) it else "open parenthesis $it close parenthesis" - } - EXPRESSIONTYPE_NOT_SET, null -> null - } -} - -private fun MathBinaryOperation.canBeReadAsImplicitMultiplication(): Boolean { - // Note that exponentiation is specialized since it's higher precedence than multiplication which - // means the graph won't look like "constant * variable" for polynomial terms like 2x^4 (which are - // cases the system should read using implicit multiplication, e.g. "two x raised to the power of - // 4"). - if (!isImplicit || !leftOperand.isConstant()) return false - return rightOperand.isVariable() || rightOperand.isExponentiation() -} - -private fun MathExpression.isConstantInteger(): Boolean = - expressionTypeCase == CONSTANT && constant.realTypeCase == INTEGER - -private fun MathExpression.isConstant(): Boolean = expressionTypeCase == CONSTANT - -private fun MathExpression.isVariable(): Boolean = expressionTypeCase == VARIABLE - -private fun MathExpression.isExponentiation(): Boolean = - expressionTypeCase == BINARY_OPERATION && binaryOperation.operator == BinaryOperator.EXPONENTIATE - -private fun MathExpression.isSingleTerm(): Boolean = when (expressionTypeCase) { - CONSTANT, VARIABLE, FUNCTION_CALL -> true - BINARY_OPERATION, UNARY_OPERATION -> false - GROUP -> group.isSingleTerm() - EXPRESSIONTYPE_NOT_SET, null -> false -} - fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) fun MathExpression.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) @@ -195,13 +28,6 @@ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() fun MathExpression.toPolynomial(): Polynomial? = stripGroups().reduceToPolynomial() -private fun Real.toPlainString(): String = when (realTypeCase) { - RATIONAL -> rational.toDouble().toPlainString() - IRRATIONAL -> irrational.toPlainString() - INTEGER -> integer.toString() - REALTYPE_NOT_SET, null -> "" -} - private fun MathExpression.stripGroups(): MathExpression { return when (expressionTypeCase) { BINARY_OPERATION -> toBuilder().apply { From 104a055cff73d41db7a6d62aba738d57018484d3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 23:45:33 -0800 Subject: [PATCH 127/289] Remove more dead code. --- .../src/main/java/org/oppia/android/util/math/BUILD.bazel | 2 -- .../oppia/android/util/math/MathExpressionExtensions.kt | 6 +++--- .../src/test/java/org/oppia/android/util/math/BUILD.bazel | 6 ------ .../oppia/android/util/math/MathExpressionParserTest.kt | 7 ++++--- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 1b594bf6fe0..3bdacb733c2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -108,8 +108,6 @@ kt_android_library( ":expression_to_latex_converter", ":expression_to_polynomial_converter", ":numeric_expression_evaluator", - ":polynomial_extensions", - "//model/src/main/proto:languages_java_proto_lite", "//model/src/main/proto:math_java_proto_lite", ], ) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index c05328d2f65..39e59ce99bb 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -17,15 +17,15 @@ import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertT import org.oppia.android.util.math.ExpressionToPolynomialConverter.Companion.reduceToPolynomial import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate -fun MathExpression.toComparableOperationList(): ComparableOperationList = - stripGroups().toComparable() - fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) fun MathExpression.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() +fun MathExpression.toComparableOperationList(): ComparableOperationList = + stripGroups().toComparable() + fun MathExpression.toPolynomial(): Polynomial? = stripGroups().reduceToPolynomial() private fun MathExpression.stripGroups(): MathExpression { diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 09b4f5a88c3..f5784442a09 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -107,18 +107,12 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", - "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_list_subject", - "//testing/src/main/java/org/oppia/android/testing/math:fraction_subject", - "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", - "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", - "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_extensions_truth-liteproto-extension", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:extensions", "//utility/src/main/java/org/oppia/android/util/math:parser", ], ) diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index d43c279b2b3..3c50966b01e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -326,17 +326,18 @@ class MathExpressionParserTest { } private fun expectFailureWhenParsingAlgebraicEquation(expression: String): MathParsingError { - val result = parseAlgebraicEquationWithAllErrors(expression) + val result = parseAlgebraicEquationInternal(expression, ErrorCheckingMode.ALL_ERRORS) assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) return (result as MathParsingResult.Failure).error } - private fun parseAlgebraicEquationWithAllErrors( + private fun parseAlgebraicEquationInternal( expression: String, + errorCheckingMode: ErrorCheckingMode, allowedVariables: List = listOf("x", "y", "z") ): MathParsingResult { return MathExpressionParser.parseAlgebraicEquation( - expression, allowedVariables, ErrorCheckingMode.ALL_ERRORS + expression, allowedVariables, errorCheckingMode ) } } From d5dd596639571e8f47bfd6659a7ba4b61bc8b431 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 23:52:17 -0800 Subject: [PATCH 128/289] Fix broken test post-refactor. --- .../NumericInputEqualsRuleClassifierProviderTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt index 1c7f3c2eb96..f7485b13545 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt @@ -11,8 +11,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder -import org.oppia.android.domain.util.FLOAT_EQUALITY_INTERVAL import org.oppia.android.testing.assertThrows +import org.oppia.android.util.math.FLOAT_EQUALITY_INTERVAL import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject From aceddf8022a6c52efe73d5302fe4b5985b38b045 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 23:57:43 -0800 Subject: [PATCH 129/289] Add reasonable import for abs(). --- .../java/org/oppia/android/util/math/FractionExtensions.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index e229fd49e40..41ca8ff2643 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -1,6 +1,7 @@ package org.oppia.android.util.math import org.oppia.android.app.model.Fraction +import kotlin.math.abs import kotlin.math.absoluteValue /** Returns whether this fraction has a fractional component. */ @@ -210,7 +211,7 @@ fun Int.toWholeNumberFraction(): Fraction { val intValue = this return Fraction.newBuilder().apply { isNegative = intValue < 0 - wholeNumber = kotlin.math.abs(intValue) + wholeNumber = abs(intValue) numerator = 0 denominator = 1 }.build() From 6e511d705d2e9f6cb2228de350b47429307697ef Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 16 Dec 2021 22:46:21 -0800 Subject: [PATCH 130/289] Integrate KotliTeX via MathTagHandler. This implementation is heavily based on #3194, including Akshay's custom fork (which was re-forked and slightly patched to work in the latest Oppia Android version). --- WORKSPACE | 8 ++ domain/src/main/assets/GJ2rLXRKD5hw_1.json | 2 +- .../src/main/assets/GJ2rLXRKD5hw_1.textproto | 2 +- third_party/BUILD.bazel | 9 +++ utility/BUILD.bazel | 1 + utility/build.gradle | 1 + .../android/util/parser/html/HtmlParser.kt | 15 ++-- .../util/parser/html/MathTagHandler.kt | 74 ++++++++++++------- 8 files changed, 79 insertions(+), 33 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index cbcd442d1b2..1a1193d1219 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -129,6 +129,14 @@ git_repository( remote = "https://github.com/oppia/androidsvg", ) +# A custom fork of KotliTeX that removes resources artifacts that break the build, and updates the +# min target SDK version to be compatible with Oppia. +git_repository( + name = "kotlitex", + commit = "26d3eb4cc148e6dae198f96a23c29d6c05cbcc56", + remote = "https://github.com/oppia/kotlitex", +) + bind( name = "databinding_annotation_processor", actual = "//tools/android:compiler_annotation_processor", diff --git a/domain/src/main/assets/GJ2rLXRKD5hw_1.json b/domain/src/main/assets/GJ2rLXRKD5hw_1.json index 40e4b2e2875..5e98aa89a01 100644 --- a/domain/src/main/assets/GJ2rLXRKD5hw_1.json +++ b/domain/src/main/assets/GJ2rLXRKD5hw_1.json @@ -4,7 +4,7 @@ "page_contents": { "subtitled_html": { "content_id": "content", - "html": "

Description of subtopic is here.

.

This is sample subtopic with dummy content related to Fractions.

Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection." + "html": "

Description of subtopic is here.

.

This is sample subtopic with dummy content related to Fractions:

Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection." }, "recorded_voiceovers": { "voiceovers_mapping": { diff --git a/domain/src/main/assets/GJ2rLXRKD5hw_1.textproto b/domain/src/main/assets/GJ2rLXRKD5hw_1.textproto index 3d9625b2b4b..85a8298d90d 100644 --- a/domain/src/main/assets/GJ2rLXRKD5hw_1.textproto +++ b/domain/src/main/assets/GJ2rLXRKD5hw_1.textproto @@ -1,6 +1,6 @@ subtopic_title: "What is a Fraction?" page_contents { - html: "

Description of subtopic is here.

.

This is sample subtopic with dummy content related to Fractions.

Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection." + html: "

Description of subtopic is here.

.

This is sample subtopic with dummy content related to Fractions:

Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection." content_id: "content" } recorded_voiceover { diff --git a/third_party/BUILD.bazel b/third_party/BUILD.bazel index 860faf7687e..ade7145d461 100644 --- a/third_party/BUILD.bazel +++ b/third_party/BUILD.bazel @@ -57,6 +57,7 @@ android_library( android_library( name = "robolectric_android-all", + testonly = True, visibility = ["//visibility:public"], exports = [ "@robolectric//bazel:android-all", @@ -73,6 +74,14 @@ java_library( ], ) +android_library( + name = "io_github_karino2_kotlitex", + visibility = ["//visibility:public"], + exports = [ + "@kotlitex//kotlitex", + ], +) + # Define a separate target for the Glide annotation processor compiler. Unfortunately, this library # can't encapsulate all of Glide (i.e. by exporting the main Glide dependency) since that includes # Android assets which java_library targets do not export. diff --git a/utility/BUILD.bazel b/utility/BUILD.bazel index 513122f87ef..080e36f0200 100644 --- a/utility/BUILD.bazel +++ b/utility/BUILD.bazel @@ -65,6 +65,7 @@ kt_android_library( "//third_party:com_github_bumptech_glide_glide", "//third_party:com_google_guava_guava", "//third_party:glide_compiler", + "//third_party:io_github_karino2_kotlitex", "//third_party:org_jetbrains_kotlinx_kotlinx-coroutines-core", "//utility/src/main/java/org/oppia/android/util/caching:annotations", "//utility/src/main/java/org/oppia/android/util/caching:asset_repository", diff --git a/utility/build.gradle b/utility/build.gradle index 412f6564637..c5a16ac0d77 100644 --- a/utility/build.gradle +++ b/utility/build.gradle @@ -65,6 +65,7 @@ dependencies { 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03', 'androidx.work:work-runtime-ktx:2.4.0', 'com.github.oppia:androidsvg:6bd15f69caee3e6857fcfcd123023716b4adec1d', + 'com.github.oppia:kotlitex:26d3eb4cc148e6dae198f96a23c29d6c05cbcc56', 'com.github.bumptech.glide:glide:4.11.0', 'com.google.dagger:dagger:2.24', 'com.google.firebase:firebase-analytics-ktx:17.5.0', diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt b/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt index 41606b659cb..30496ad07db 100755 --- a/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt @@ -1,5 +1,6 @@ package org.oppia.android.util.parser.html +import android.content.Context import android.text.Spannable import android.text.SpannableStringBuilder import android.text.method.LinkMovementMethod @@ -12,6 +13,7 @@ import javax.inject.Inject /** Html Parser to parse custom Oppia tags with Android-compatible versions. */ class HtmlParser private constructor( + private val context: Context, private val urlImageParserFactory: UrlImageParser.Factory, private val gcsResourceName: String, private val entityType: String, @@ -32,7 +34,6 @@ class HtmlParser private constructor( } private val bulletTagHandler by lazy { BulletTagHandler() } private val imageTagHandler by lazy { ImageTagHandler(consoleLogger) } - private val mathTagHandler by lazy { MathTagHandler(consoleLogger) } /** * Parses a raw HTML string with support for custom Oppia tags. @@ -84,7 +85,7 @@ class HtmlParser private constructor( htmlContentTextView, gcsResourceName, entityType, entityId, imageCenterAlign ) val htmlSpannable = CustomHtmlContentHandler.fromHtml( - htmlContent, imageGetter, computeCustomTagHandlers(supportsConceptCards) + htmlContent, imageGetter, computeCustomTagHandlers(supportsConceptCards, htmlContentTextView) ) val spannableBuilder = CustomBulletSpan.replaceBulletSpan( @@ -99,12 +100,14 @@ class HtmlParser private constructor( } private fun computeCustomTagHandlers( - supportsConceptCards: Boolean + supportsConceptCards: Boolean, + htmlContentTextView: TextView ): Map { val handlersMap = mutableMapOf() handlersMap[CUSTOM_BULLET_LIST_TAG] = bulletTagHandler handlersMap[CUSTOM_IMG_TAG] = imageTagHandler - handlersMap[CUSTOM_MATH_TAG] = mathTagHandler + handlersMap[CUSTOM_MATH_TAG] = + MathTagHandler(consoleLogger, context.assets, htmlContentTextView.lineHeight.toFloat()) if (supportsConceptCards) { handlersMap[CUSTOM_CONCEPT_CARD_TAG] = conceptCardTagHandler } @@ -143,7 +146,8 @@ class HtmlParser private constructor( /** Factory for creating new [HtmlParser]s. */ class Factory @Inject constructor( private val urlImageParserFactory: UrlImageParser.Factory, - private val consoleLogger: ConsoleLogger + private val consoleLogger: ConsoleLogger, + private val context: Context ) { /** * Returns a new [HtmlParser] with the specified entity type and ID for loading images, and an @@ -157,6 +161,7 @@ class HtmlParser private constructor( customOppiaTagActionListener: CustomOppiaTagActionListener? = null ): HtmlParser { return HtmlParser( + context, urlImageParserFactory, gcsResourceName, entityType, diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt index 0e2329e1bae..af2fc83b14f 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt @@ -1,8 +1,10 @@ package org.oppia.android.util.parser.html +import android.content.res.AssetManager import android.text.Editable import android.text.Spannable import android.text.style.ImageSpan +import io.github.karino2.kotlitex.view.MathExpressionSpan import org.json.JSONObject import org.oppia.android.util.logging.ConsoleLogger import org.xml.sax.Attributes @@ -16,7 +18,9 @@ private const val CUSTOM_MATH_SVG_PATH_ATTRIBUTE = "math_content-with-value" * [CustomHtmlContentHandler]. */ class MathTagHandler( - private val consoleLogger: ConsoleLogger + private val consoleLogger: ConsoleLogger, + private val assetManager: AssetManager, + private val lineHeight: Float ) : CustomHtmlContentHandler.CustomTagHandler { override fun handleTag( attributes: Attributes, @@ -29,39 +33,57 @@ class MathTagHandler( val content = MathContent.parseMathContent( attributes.getJsonObjectValue(CUSTOM_MATH_SVG_PATH_ATTRIBUTE) ) - if (content != null) { - // Insert an image span where the custom tag currently is to load the SVG. In the future, this - // could also load a LaTeX span, instead. Note that this approach is based on Android's Html - // parser. - val drawable = - imageRetriever.loadDrawable( - content.svgFilename, - CustomHtmlContentHandler.ImageRetriever.Type.INLINE_TEXT_IMAGE + val newSpan = when (content) { + is MathContent.MathAsSvg -> { + ImageSpan( + imageRetriever.loadDrawable( + content.svgFilename, + CustomHtmlContentHandler.ImageRetriever.Type.INLINE_TEXT_IMAGE + ), + content.svgFilename ) - val (startIndex, endIndex) = output.run { - // Use a control character to ensure that there's at least 1 character on which to "attach" - // the image when rendering the HTML. - val startIndex = length - append('\uFFFC') - return@run startIndex to length } - output.setSpan( - ImageSpan(drawable, content.svgFilename), - startIndex, - endIndex, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } else consoleLogger.e("MathTagHandler", "Failed to parse math tag") + is MathContent.MathAsLatex -> { + MathExpressionSpan(content.rawLatex, lineHeight, assetManager, isMathMode = true) + } + null -> { + consoleLogger.e("MathTagHandler", "Failed to parse math tag") + return + } + } + + // Insert an image span where the custom tag currently is to load the SVG/LaTeX span. Note that + // this approach is based on Android's HTML parser. + val (startIndex, endIndex) = output.run { + // Use a control character to ensure that there's at least 1 character on which to + // "attach" the image when rendering the HTML. + val startIndex = length + append('\uFFFC') + return@run startIndex to length + } + output.setSpan( + newSpan, + startIndex, + endIndex, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) } - private data class MathContent(val rawLatex: String, val svgFilename: String) { + private sealed class MathContent { + data class MathAsSvg(val svgFilename: String) : MathContent() + + data class MathAsLatex(val rawLatex: String) : MathContent() + companion object { internal fun parseMathContent(obj: JSONObject?): MathContent? { + // Kotlitex expects escaped backslashes. val rawLatex = obj?.getOptionalString("raw_latex") val svgFilename = obj?.getOptionalString("svg_filename") - return if (rawLatex != null && svgFilename != null) { - MathContent(rawLatex, svgFilename) - } else null + return when { + svgFilename != null -> MathAsSvg(svgFilename) + rawLatex != null -> MathAsLatex(rawLatex) + else -> null + } } /** From 754d42df252d549ecd7007f462853dbb7387f1c5 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 16 Dec 2021 23:38:24 -0800 Subject: [PATCH 131/289] Add configurable inline LaTeX rendering support. --- .../org/oppia/android/util/parser/html/HtmlParser.kt | 12 ++++++++++-- .../oppia/android/util/parser/html/MathTagHandler.kt | 7 +++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt b/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt index 30496ad07db..75601fc8a4b 100755 --- a/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt @@ -19,6 +19,7 @@ class HtmlParser private constructor( private val entityType: String, private val entityId: String, private val imageCenterAlign: Boolean, + private val useInlineMathRendering: Boolean, private val consoleLogger: ConsoleLogger, customOppiaTagActionListener: CustomOppiaTagActionListener? ) { @@ -107,7 +108,12 @@ class HtmlParser private constructor( handlersMap[CUSTOM_BULLET_LIST_TAG] = bulletTagHandler handlersMap[CUSTOM_IMG_TAG] = imageTagHandler handlersMap[CUSTOM_MATH_TAG] = - MathTagHandler(consoleLogger, context.assets, htmlContentTextView.lineHeight.toFloat()) + MathTagHandler( + consoleLogger, + context.assets, + htmlContentTextView.lineHeight.toFloat(), + useInlineMathRendering + ) if (supportsConceptCards) { handlersMap[CUSTOM_CONCEPT_CARD_TAG] = conceptCardTagHandler } @@ -158,7 +164,8 @@ class HtmlParser private constructor( entityType: String, entityId: String, imageCenterAlign: Boolean, - customOppiaTagActionListener: CustomOppiaTagActionListener? = null + customOppiaTagActionListener: CustomOppiaTagActionListener? = null, + useInlineMathRendering: Boolean = true ): HtmlParser { return HtmlParser( context, @@ -167,6 +174,7 @@ class HtmlParser private constructor( entityType, entityId, imageCenterAlign, + useInlineMathRendering, consoleLogger, customOppiaTagActionListener ) diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt index af2fc83b14f..481bfbed042 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt @@ -20,7 +20,8 @@ private const val CUSTOM_MATH_SVG_PATH_ATTRIBUTE = "math_content-with-value" class MathTagHandler( private val consoleLogger: ConsoleLogger, private val assetManager: AssetManager, - private val lineHeight: Float + private val lineHeight: Float, + private val useInlineRendering: Boolean ) : CustomHtmlContentHandler.CustomTagHandler { override fun handleTag( attributes: Attributes, @@ -44,7 +45,9 @@ class MathTagHandler( ) } is MathContent.MathAsLatex -> { - MathExpressionSpan(content.rawLatex, lineHeight, assetManager, isMathMode = true) + MathExpressionSpan( + content.rawLatex, lineHeight, assetManager, isMathMode = !useInlineRendering + ) } null -> { consoleLogger.e("MathTagHandler", "Failed to parse math tag") From 70b00d10eae9f4034d52145a2f79fd47acad71e5 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 5 Jan 2022 15:50:56 -0800 Subject: [PATCH 132/289] Add debug menu for math expressions. This commit adds a new debug menu for testing math (algebraic & numeric) expressions/equations, including supporting adding different output types (such as LaTeX, math expressions, polynomials, and others). The menu has already helped discover two issues with the system: a missing feature in KotliTex around larger square roots, and a mistake when rooting fractions. Both of these were fixed upstream. --- app/BUILD.bazel | 4 + app/src/main/AndroidManifest.xml | 4 + .../app/activity/ActivityComponentImpl.kt | 2 + .../devoptions/DeveloperOptionsActivity.kt | 8 +- .../DeveloperOptionsFragmentPresenter.kt | 15 +- .../devoptions/DeveloperOptionsViewModel.kt | 6 +- ...RouteToMathExpressionParserTestListener.kt | 7 + .../DeveloperOptionsTestParsersViewModel.kt | 16 ++ .../MathExpressionParserActivity.kt | 33 +++ .../MathExpressionParserActivityPresenter.kt | 33 +++ .../MathExpressionParserFragment.kt | 34 +++ .../MathExpressionParserFragmentPresenter.kt | 39 +++ .../MathExpressionParserViewModel.kt | 179 +++++++++++++ .../app/fragment/FragmentComponentImpl.kt | 2 + .../developer_options_test_parsers_view.xml | 43 +++ .../math_expression_parser_activity.xml | 5 + .../math_expression_parser_fragment.xml | 253 ++++++++++++++++++ .../main/res/values/untranslated_strings.xml | 18 ++ 18 files changed, 698 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/org/oppia/android/app/devoptions/RouteToMathExpressionParserTestListener.kt create mode 100644 app/src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsTestParsersViewModel.kt create mode 100644 app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserActivity.kt create mode 100644 app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserActivityPresenter.kt create mode 100644 app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserFragment.kt create mode 100644 app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserFragmentPresenter.kt create mode 100644 app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserViewModel.kt create mode 100644 app/src/main/res/layout/developer_options_test_parsers_view.xml create mode 100644 app/src/main/res/layout/math_expression_parser_activity.xml create mode 100644 app/src/main/res/layout/math_expression_parser_fragment.xml diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 80d5ca54a6d..9eefdcf9f03 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -93,6 +93,7 @@ LISTENERS = [ "src/main/java/org/oppia/android/app/devoptions/RouteToMarkChaptersCompletedListener.kt", "src/main/java/org/oppia/android/app/devoptions/RouteToMarkStoriesCompletedListener.kt", "src/main/java/org/oppia/android/app/devoptions/RouteToMarkTopicsCompletedListener.kt", + "src/main/java/org/oppia/android/app/devoptions/RouteToMathExpressionParserTestListener.kt", "src/main/java/org/oppia/android/app/devoptions/RouteToViewEventLogsListener.kt", "src/main/java/org/oppia/android/app/drawer/RouteToProfileProgressListener.kt", "src/main/java/org/oppia/android/app/help/LoadFaqListFragmentListener.kt", @@ -169,6 +170,7 @@ DATABINDING_LAYOUTS = ["src/main/res/layout*/**"] VIEW_MODELS_WITH_RESOURCE_IMPORTS = [ "src/main/java/org/oppia/android/app/administratorcontrols/appversion/AppVersionViewModel.kt", "src/main/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeViewModel.kt", + "src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserViewModel.kt", "src/main/java/org/oppia/android/app/drawer/NavigationDrawerHeaderViewModel.kt", "src/main/java/org/oppia/android/app/help/HelpItemViewModel.kt", "src/main/java/org/oppia/android/app/help/HelpListViewModel.kt", @@ -233,6 +235,7 @@ VIEW_MODELS = [ "src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsItemViewModel.kt", "src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsModifyLessonProgressViewModel.kt", "src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsOverrideAppBehaviorsViewModel.kt", + "src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsTestParsersViewModel.kt", "src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsViewLogsViewModel.kt", "src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsViewModel.kt", "src/main/java/org/oppia/android/app/devoptions/forcenetworktype/NetworkTypeItemViewModel.kt", @@ -644,6 +647,7 @@ kt_android_library( "//app/src/main/java/org/oppia/android/app/viewmodel:observable_view_model", "//app/src/main/java/org/oppia/android/app/viewmodel:view_model_provider", "//app/src/main/java/org/oppia/android/app/utility/datetime:date_time_util", + "//app/src/main/java/org/oppia/android/app/utility/math:math_expression_accessibility_util", "//domain", "//domain/src/main/java/org/oppia/android/domain/audio:audio_player_controller", "//domain/src/main/java/org/oppia/android/domain/onboarding:state_controller", diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3ade708d1f0..7c3f9f7e986 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -260,6 +260,10 @@ + { + viewModel.itemIndex.set(3) + ViewType.VIEW_TYPE_TEST_PARSERS + } else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") } } @@ -93,12 +99,19 @@ class DeveloperOptionsFragmentPresenter @Inject constructor( setViewModel = DeveloperOptionsOverrideAppBehaviorsViewBinding::setViewModel, transformViewModel = { it as DeveloperOptionsOverrideAppBehaviorsViewModel } ) + .registerViewDataBinder( + viewType = ViewType.VIEW_TYPE_TEST_PARSERS, + inflateDataBinding = DeveloperOptionsTestParsersViewBinding::inflate, + setViewModel = DeveloperOptionsTestParsersViewBinding::setViewModel, + transformViewModel = { it as DeveloperOptionsTestParsersViewModel } + ) .build() } private enum class ViewType { VIEW_TYPE_MODIFY_LESSON_PROGRESS, VIEW_TYPE_VIEW_LOGS, - VIEW_TYPE_OVERRIDE_APP_BEHAVIORS + VIEW_TYPE_OVERRIDE_APP_BEHAVIORS, + VIEW_TYPE_TEST_PARSERS } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsViewModel.kt index a1724ce1485..448d546709f 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsViewModel.kt @@ -9,6 +9,7 @@ import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptions import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.domain.devoptions.ShowAllHintsAndSolutionController import javax.inject.Inject +import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsTestParsersViewModel /** * [ViewModel] for [DeveloperOptionsFragment]. It populates the recyclerview with a list of @@ -28,6 +29,8 @@ class DeveloperOptionsViewModel @Inject constructor( activity as RouteToMarkTopicsCompletedListener private val routeToViewEventLogsListener = activity as RouteToViewEventLogsListener private val routeToForceNetworkTypeListener = activity as RouteToForceNetworkTypeListener + private val routeToMathExpressionParserTestListener = + activity as RouteToMathExpressionParserTestListener /** * List of [DeveloperOptionsItemViewModel] used to populate recyclerview of @@ -49,7 +52,8 @@ class DeveloperOptionsViewModel @Inject constructor( forceCrashButtonClickListener, routeToForceNetworkTypeListener, showAllHintsAndSolutionController - ) + ), + DeveloperOptionsTestParsersViewModel(routeToMathExpressionParserTestListener) ) } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/RouteToMathExpressionParserTestListener.kt b/app/src/main/java/org/oppia/android/app/devoptions/RouteToMathExpressionParserTestListener.kt new file mode 100644 index 00000000000..daf68a254d9 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/devoptions/RouteToMathExpressionParserTestListener.kt @@ -0,0 +1,7 @@ +package org.oppia.android.app.devoptions + +/** Listener for when the user wants to test math expressions/equations. */ +interface RouteToMathExpressionParserTestListener { + /** Called when the user indicates that they want to test math expressions/equations. */ + fun routeToMathExpressionParserTest() +} diff --git a/app/src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsTestParsersViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsTestParsersViewModel.kt new file mode 100644 index 00000000000..00e7edf8f56 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsTestParsersViewModel.kt @@ -0,0 +1,16 @@ +package org.oppia.android.app.devoptions.devoptionsitemviewmodel + +import org.oppia.android.app.devoptions.RouteToMathExpressionParserTestListener + +/** + * [DeveloperOptionsItemViewModel] to provide features to test and debug math expressions and + * equations. + */ +class DeveloperOptionsTestParsersViewModel( + private val routeToMathExpressionParserTestListener: RouteToMathExpressionParserTestListener +) : DeveloperOptionsItemViewModel() { + /** Routes the user to an activity for testing math expressions & equations. */ + fun onMathExpressionsClicked() { + routeToMathExpressionParserTestListener.routeToMathExpressionParserTest() + } +} diff --git a/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserActivity.kt b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserActivity.kt new file mode 100644 index 00000000000..25349699375 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserActivity.kt @@ -0,0 +1,33 @@ +package org.oppia.android.app.devoptions.mathexpressionparser + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import org.oppia.android.R +import org.oppia.android.app.activity.ActivityComponentImpl +import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.translation.AppLanguageResourceHandler +import javax.inject.Inject + +/** Activity to allow the user to test math expressions/equations. */ +class MathExpressionParserActivity : InjectableAppCompatActivity() { + @Inject + lateinit var mathExpressionParserActivityPresenter: MathExpressionParserActivityPresenter + + @Inject + lateinit var resourceHandler: AppLanguageResourceHandler + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (activityComponent as ActivityComponentImpl).inject(this) + mathExpressionParserActivityPresenter.handleOnCreate() + title = resourceHandler.getStringInLocale(R.string.math_expression_parser_activity_title) + } + + companion object { + /** Returns [Intent] for [MathExpressionParserActivity]. */ + fun createIntent(context: Context): Intent { + return Intent(context, MathExpressionParserActivity::class.java) + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserActivityPresenter.kt new file mode 100644 index 00000000000..000f0fda718 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserActivityPresenter.kt @@ -0,0 +1,33 @@ +package org.oppia.android.app.devoptions.mathexpressionparser + +import androidx.appcompat.app.AppCompatActivity +import org.oppia.android.R +import org.oppia.android.app.activity.ActivityScope +import javax.inject.Inject + +/** The presenter for [MathExpressionParserActivity]. */ +@ActivityScope +class MathExpressionParserActivityPresenter @Inject constructor( + private val activity: AppCompatActivity +) { + + /** Called when [MathExpressionParserActivity] is created. Handles UI for the activity. */ + fun handleOnCreate() { + activity.supportActionBar?.setDisplayHomeAsUpEnabled(true) + activity.supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_arrow_back_white_24dp) + activity.setContentView(R.layout.math_expression_parser_activity) + + if (getMathExpressionParserFragment() == null) { + val forceNetworkTypeFragment = MathExpressionParserFragment.newInstance() + activity.supportFragmentManager.beginTransaction().add( + R.id.math_expression_parser_container, + forceNetworkTypeFragment + ).commitNow() + } + } + + private fun getMathExpressionParserFragment(): MathExpressionParserFragment? { + return activity.supportFragmentManager + .findFragmentById(R.id.force_network_type_container) as MathExpressionParserFragment? + } +} diff --git a/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserFragment.kt b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserFragment.kt new file mode 100644 index 00000000000..b11b489df5b --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserFragment.kt @@ -0,0 +1,34 @@ +package org.oppia.android.app.devoptions.mathexpressionparser + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.oppia.android.app.fragment.FragmentComponentImpl +import org.oppia.android.app.fragment.InjectableFragment +import javax.inject.Inject + +/** Fragment to provide user testing support for math expressions/equations. */ +class MathExpressionParserFragment : InjectableFragment() { + @Inject + lateinit var mathExpressionParserFragmentPresenter: MathExpressionParserFragmentPresenter + + companion object { + /** Returns a new instance of [MathExpressionParserFragment]. */ + fun newInstance(): MathExpressionParserFragment = MathExpressionParserFragment() + } + + override fun onAttach(context: Context) { + super.onAttach(context) + (fragmentComponent as FragmentComponentImpl).inject(this) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return mathExpressionParserFragmentPresenter.handleCreateView(inflater, container) + } +} diff --git a/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserFragmentPresenter.kt new file mode 100644 index 00000000000..29e9409d59a --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserFragmentPresenter.kt @@ -0,0 +1,39 @@ +package org.oppia.android.app.devoptions.mathexpressionparser + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import javax.inject.Inject +import org.oppia.android.databinding.MathExpressionParserFragmentBinding + +/** The presenter for [MathExpressionParserFragment]. */ +class MathExpressionParserFragmentPresenter @Inject constructor( + private val activity: AppCompatActivity, + private val fragment: Fragment, + private val viewModel: MathExpressionParserViewModel +) { + /** Called when [MathExpressionParserFragment] is created. Handles UI for the fragment. */ + fun handleCreateView( + inflater: LayoutInflater, + container: ViewGroup? + ): View { + val binding = MathExpressionParserFragmentBinding.inflate( + inflater, + container, + /* attachToRoot= */ false + ) + + binding.mathExpressionParserToolbar.setNavigationOnClickListener { + (activity as MathExpressionParserActivity).finish() + } + + binding.apply { + lifecycleOwner = fragment + viewModel = this@MathExpressionParserFragmentPresenter.viewModel + } + viewModel.initialize(binding.mathExpressionParseResultTextView) + return binding.root + } +} diff --git a/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserViewModel.kt new file mode 100644 index 00000000000..065d6a31852 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserViewModel.kt @@ -0,0 +1,179 @@ +package org.oppia.android.app.devoptions.mathexpressionparser + +import android.widget.TextView +import androidx.databinding.ObservableField +import javax.inject.Inject +import org.oppia.android.R +import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.OppiaLanguage +import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.app.utility.math.MathExpressionAccessibilityUtil +import org.oppia.android.app.viewmodel.ObservableViewModel +import org.oppia.android.util.locale.OppiaLocale +import org.oppia.android.util.math.MathExpressionParser +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.toComparableOperationList +import org.oppia.android.util.math.toPolynomial +import org.oppia.android.util.math.toRawLatex +import org.oppia.android.util.parser.html.HtmlParser + +/** + * View model that provides different debugging scenarios for math expressions, equations, and + * numeric expressions. + */ +@FragmentScope +class MathExpressionParserViewModel @Inject constructor( + private val appLanguageResourceHandler: AppLanguageResourceHandler, + private val machineLocale: OppiaLocale.MachineLocale, + private val mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil, + private val htmlParserFactory: HtmlParser.Factory +) : ObservableViewModel() { + private val htmlParser by lazy { + // TODO: replace this with a variant that doesn't require the GCS properties. + htmlParserFactory.create("", "", "", false) + } + private lateinit var parseResultTextView: TextView + var mathExpression = ObservableField() + var allowedVariables = ObservableField("x,y") + private var parseType = ParseType.NUMERIC_EXPRESSION + private var resultType = ResultType.MATH_EXPRESSION + private var useDivAsFractions = false + + fun initialize(parseResultTextView: TextView) { + this.parseResultTextView = parseResultTextView + updateParseResult() + } + + fun onParseButtonClicked() { + updateParseResult() + } + + fun onParseTypeSelected(parseType: ParseType) { + this.parseType = parseType + } + + fun onResultTypeSelected(resultType: ResultType) { + this.resultType = resultType + } + + fun onChangedUseDivAsFractions(useDivAsFractions: Boolean) { + this.useDivAsFractions = useDivAsFractions + } + + private fun updateParseResult() { + val newText = computeParseResult() + // Only parse HTML if there is HTML to preserve formatting. + parseResultTextView.text = if ("oppia-noninteractive-math" in newText) { + htmlParser.parseOppiaHtml(newText.replace("\n", "
"), parseResultTextView) + } else newText + } + + private fun computeParseResult(): String { + val expression = mathExpression.get() + val allowedVariables = allowedVariables.get() + ?.split(",") + ?.map { variable -> + machineLocale.run { + variable.toMachineLowerCase().trim() + } + } ?: listOf() + if (expression == null) { + return appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_parse_result_label, "Uninitialized" + ) + } + val parseResult = when (parseType) { + ParseType.NUMERIC_EXPRESSION -> { + MathExpressionParser.parseNumericExpression(expression) + .transformExpression(resultType, useDivAsFractions, mathExpressionAccessibilityUtil) + } + ParseType.ALGEBRAIC_EXPRESSION -> { + MathExpressionParser.parseAlgebraicExpression(expression, allowedVariables) + .transformExpression(resultType, useDivAsFractions, mathExpressionAccessibilityUtil) + } + ParseType.ALGEBRAIC_EQUATION -> { + MathExpressionParser.parseAlgebraicEquation(expression, allowedVariables) + .transformEquation(resultType, useDivAsFractions, mathExpressionAccessibilityUtil) + } + } + val parseResultStr = when (parseResult) { + is MathParsingResult.Failure -> parseResult.error.toString() + is MathParsingResult.Success -> parseResult.result + } + return appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_parse_result_label, "\n$parseResultStr" + ) + } + + enum class ParseType { + NUMERIC_EXPRESSION, + ALGEBRAIC_EXPRESSION, + ALGEBRAIC_EQUATION + } + + enum class ResultType { + MATH_EXPRESSION, + COMPARABLE_OPERATION_LIST, + POLYNOMIAL, + LATEX, + HUMAN_READABLE_STRING + } + + private companion object { + private fun MathParsingResult.map(transform: (I) -> O): MathParsingResult { + return when (this) { + is MathParsingResult.Failure -> MathParsingResult.Failure(error) + is MathParsingResult.Success -> MathParsingResult.Success(transform(result)) + } + } + + private fun MathParsingResult.transformExpression( + resultType: ResultType, + useDivAsFractions: Boolean, + mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil + ): MathParsingResult { + return when (resultType) { + ResultType.MATH_EXPRESSION -> this + ResultType.COMPARABLE_OPERATION_LIST -> map { it.toComparableOperationList() } + ResultType.POLYNOMIAL -> map { it.toPolynomial() } + ResultType.LATEX -> map { it.toRawLatex(useDivAsFractions).wrapAsLatexHtml() } + ResultType.HUMAN_READABLE_STRING -> map { + mathExpressionAccessibilityUtil.convertToHumanReadableString( + it, OppiaLanguage.ENGLISH, useDivAsFractions + ) + } + }.map { it.toString() } + } + + private fun MathParsingResult.transformEquation( + resultType: ResultType, + useDivAsFractions: Boolean, + mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil + ): MathParsingResult { + return when (resultType) { + ResultType.MATH_EXPRESSION -> this + ResultType.COMPARABLE_OPERATION_LIST -> map { + "Left side: ${it.leftSide.toComparableOperationList()}" + + "\n\nRight side: ${it.rightSide.toComparableOperationList()}" + } + ResultType.POLYNOMIAL -> map { + "Left side: ${it.leftSide.toPolynomial()}\n\nRight side: ${it.rightSide.toPolynomial()}" + } + ResultType.LATEX -> map { it.toRawLatex(useDivAsFractions).wrapAsLatexHtml() } + ResultType.HUMAN_READABLE_STRING -> map { + mathExpressionAccessibilityUtil.convertToHumanReadableString( + it, OppiaLanguage.ENGLISH, useDivAsFractions + ) + } + }.map { it.toString() } + } + + private fun String.wrapAsLatexHtml(): String { + val mathContentValue = + "{&quot;raw_latex&quot;:&quot;${this.replace("\\", "\\\\")}&quot;}" + return "" + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt b/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt index 439a87567d0..315d4e3246c 100644 --- a/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt +++ b/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt @@ -13,6 +13,7 @@ import org.oppia.android.app.devoptions.forcenetworktype.ForceNetworkTypeFragmen import org.oppia.android.app.devoptions.markchapterscompleted.MarkChaptersCompletedFragment import org.oppia.android.app.devoptions.markstoriescompleted.MarkStoriesCompletedFragment import org.oppia.android.app.devoptions.marktopicscompleted.MarkTopicsCompletedFragment +import org.oppia.android.app.devoptions.mathexpressionparser.MathExpressionParserFragment import org.oppia.android.app.devoptions.vieweventlogs.ViewEventLogsFragment import org.oppia.android.app.drawer.ExitProfileDialogFragment import org.oppia.android.app.drawer.NavigationDrawerFragment @@ -124,6 +125,7 @@ interface FragmentComponentImpl : FragmentComponent, ViewComponentBuilderInjecto fun inject(markChapterCompletedFragment: MarkChaptersCompletedFragment) fun inject(markStoriesCompletedFragment: MarkStoriesCompletedFragment) fun inject(markTopicsCompletedFragment: MarkTopicsCompletedFragment) + fun inject(mathExpressionParserFragment: MathExpressionParserFragment) fun inject(myDownloadsFragment: MyDownloadsFragment) fun inject(navigationDrawerFragment: NavigationDrawerFragment) fun inject(onboardingFragment: OnboardingFragment) diff --git a/app/src/main/res/layout/developer_options_test_parsers_view.xml b/app/src/main/res/layout/developer_options_test_parsers_view.xml new file mode 100644 index 00000000000..db36adc6b6a --- /dev/null +++ b/app/src/main/res/layout/developer_options_test_parsers_view.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/math_expression_parser_activity.xml b/app/src/main/res/layout/math_expression_parser_activity.xml new file mode 100644 index 00000000000..24d77b2cb9c --- /dev/null +++ b/app/src/main/res/layout/math_expression_parser_activity.xml @@ -0,0 +1,5 @@ + + diff --git a/app/src/main/res/layout/math_expression_parser_fragment.xml b/app/src/main/res/layout/math_expression_parser_fragment.xml new file mode 100644 index 00000000000..77e11d081ef --- /dev/null +++ b/app/src/main/res/layout/math_expression_parser_fragment.xml @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + +